├── .venv ├── tests ├── __init__.py └── changes │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── serializer │ │ ├── __init__.py │ │ └── models │ │ │ ├── __init__.py │ │ │ ├── test_cause.py │ │ │ ├── test_project.py │ │ │ ├── test_buildmessage.py │ │ │ ├── test_bazeltargetmessage.py │ │ │ ├── test_adminmessage.py │ │ │ ├── test_change.py │ │ │ └── test_logchunk.py │ ├── validators │ │ ├── __init__.py │ │ ├── test_basic.py │ │ └── test_datetime.py │ ├── test_client.py │ ├── test_node_details.py │ ├── test_cluster_details.py │ ├── test_change_details.py │ ├── test_node_index.py │ ├── test_cluster_index.py │ ├── test_testcase_details.py │ ├── test_project_plan_index.py │ ├── test_cluster_nodes.py │ ├── test_node_from_hostname.py │ ├── test_build_mark_seen.py │ ├── test_patch_details.py │ ├── test_repository_project_index.py │ ├── test_project_source_build_index.py │ ├── test_build_message_index.py │ ├── test_task_index.py │ ├── test_job_artifact_index.py │ └── test_build_target_message_index.py │ ├── jobs │ └── __init__.py │ ├── lib │ └── __init__.py │ ├── models │ └── __init__.py │ ├── queue │ └── __init__.py │ ├── storage │ └── __init__.py │ ├── utils │ ├── __init__.py │ ├── test_trees.py │ └── test_http.py │ ├── vcs │ └── __init__.py │ ├── web │ └── __init__.py │ ├── artifacts │ └── __init__.py │ ├── backends │ ├── __init__.py │ └── jenkins │ │ ├── __init__.py │ │ ├── buildsteps │ │ └── __init__.py │ │ └── fixtures │ │ └── GET │ │ ├── queue_item_by_job_id.xml │ │ ├── job_list_by_upstream.xml │ │ ├── build_item_by_job_id.xml │ │ └── node_config.xml │ ├── listeners │ └── __init__.py │ └── test_config.py ├── changes ├── api │ ├── __init__.py │ ├── validators │ │ ├── __init__.py │ │ ├── basic.py │ │ ├── datetime.py │ │ └── author.py │ ├── serializer │ │ ├── vcs │ │ │ ├── __init__.py │ │ │ └── revisionresult.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── latest_green_build.py │ │ │ ├── node.py │ │ │ ├── cluster.py │ │ │ ├── comment.py │ │ │ ├── adminmessage.py │ │ │ ├── buildmessage.py │ │ │ ├── testartifact.py │ │ │ ├── bazeltargetmessage.py │ │ │ ├── artifact.py │ │ │ ├── revisionresult.py │ │ │ ├── snapshotimage.py │ │ │ ├── logchunk.py │ │ │ ├── author.py │ │ │ ├── event.py │ │ │ ├── patch.py │ │ │ ├── project.py │ │ │ ├── change.py │ │ │ ├── jobphase.py │ │ │ ├── command.py │ │ │ └── task.py │ │ └── __init__.py │ ├── cluster_index.py │ ├── cached_snapshot_cluster_details.py │ ├── change_details.py │ ├── node_details.py │ ├── build_coverage.py │ ├── patch_details.py │ ├── build_message_index.py │ ├── node_from_hostname.py │ ├── repository_project_index.py │ ├── cluster_details.py │ ├── build_target_message_index.py │ ├── auth_index.py │ ├── job_artifact_index.py │ ├── task_index.py │ ├── build_mark_seen.py │ ├── controller.py │ ├── task_details.py │ ├── build_test_index_counts.py │ ├── node_job_index.py │ └── snapshotimage_details.py ├── db │ ├── __init__.py │ ├── types │ │ ├── __init__.py │ │ └── enum.py │ └── funcs │ │ ├── __init__.py │ │ └── coalesce.py ├── ext │ └── __init__.py ├── vcs │ └── __init__.py ├── web │ └── __init__.py ├── backends │ ├── __init__.py │ ├── jenkins │ │ ├── __init__.py │ │ └── buildsteps │ │ │ └── __init__.py │ └── base.py ├── debug │ ├── __init__.py │ ├── mail │ │ ├── __init__.py │ │ └── build_result.py │ └── reports │ │ ├── __init__.py │ │ └── build.py ├── jobs │ ├── __init__.py │ ├── generate_report.py │ └── check_repos.py ├── models │ ├── __init__.py │ └── itemsequence.py ├── queue │ └── __init__.py ├── reports │ └── __init__.py ├── storage │ ├── __init__.py │ └── base.py ├── utils │ ├── __init__.py │ ├── times.py │ ├── dirs.py │ ├── cache.py │ ├── slugs.py │ └── locking.py ├── artifacts │ ├── __init__.py │ └── dummylogfile.py ├── buildsteps │ ├── __init__.py │ ├── dummy.py │ └── lxc.py ├── expanders │ └── __init__.py ├── listeners │ ├── __init__.py │ └── snapshot_build.py ├── lib │ ├── __init__.py │ ├── build_lib.py │ └── project_lib.py ├── url_converters │ ├── __init__.py │ └── uuid.py ├── experimental │ └── __init__.py ├── testutils │ └── __init__.py ├── buildfailures │ ├── base.py │ ├── timeout.py │ ├── malformedartifact.py │ ├── duplicatetestname.py │ ├── missingmanifestjson.py │ ├── missingtests.py │ ├── missingtargets.py │ ├── malformedmanifestjson.py │ ├── missingartifact.py │ └── testfailure.py └── app.py ├── linters └── __init__.py ├── test_simple ├── .eslintignore ├── static └── vendor │ ├── requirejs-babel │ ├── .gitignore │ ├── bower.json │ ├── README.md │ └── .bower.json │ ├── classnames │ ├── .npmignore │ ├── .travis.yml │ └── bower.json │ ├── react │ ├── bower.json │ ├── .bower.json │ ├── react-dom.min.js │ └── react-dom-server.min.js │ ├── requirejs │ ├── README.md │ ├── bower.json │ └── .bower.json │ ├── uri.js │ ├── bower.json │ └── .bower.json │ ├── underscore │ ├── bower.json │ └── .bower.json │ ├── moment │ ├── component.json │ ├── lang │ │ └── readme.md │ └── .bower.json │ └── react-bootstrap │ ├── bower.json │ ├── README.md │ └── .bower.json ├── .bowerrc ├── migrations ├── README ├── versions │ ├── 4134b4818694_remove_index_build_p.py │ ├── fe743605e1a_remove_repositoryoption.py │ ├── 5aad6f742b77_parent_revision_sha_.py │ ├── 4f955feb6d07_rename_build_job.py │ ├── f2c8d15416b_index_build_project_.py │ ├── 105d4dd82a0a_date_indexes_on_test.py │ ├── 2622a69cd25a_unique_node_label.py │ ├── 4901f27ade8e_fill_target.py │ ├── 5508859bed73_index_source_patch_i.py │ ├── 34157be0e8a2_index_patch_project_.py │ ├── 42e9d35a4098_rename_buildstep_job.py │ ├── 545ba163595_rename_buildfamily_b.py │ ├── 6483270c001_rename_buildplan_job.py │ ├── 516f04a1a754_rename_buildphase_jo.py │ ├── 97786e74292_add_task_data.py │ ├── 4ae598236ef1_unused_indexes_on_jo.py │ ├── 181adec926e2_add_status_index_to_task.py │ ├── 21b7c3b2ce88_index_build_project_.py │ ├── 36d7c98ddfee_drop_projectplan_table.py │ ├── 21a9d1ebe15c_add_jobplan_data.py │ ├── 26c0affcb18a_add_jobstep_data.py │ ├── 315e787e94bf_add_source_data.py │ ├── 4ba1dd8c3080_add_index_for_projec.py │ ├── 4d235d421320_add_artifact_file.py │ ├── bb141e41aab_add_rerun_count_to_t.py │ ├── f8f72eecc7f_remove_idx_testgroup.py │ ├── 26f665189ca0_remove_jobstep_jobph.py │ ├── 2b4219a6cf46_jobphase_build_id_jo.py │ ├── 35af40cebcde_jobplan_build_id_job.py │ ├── f26b6cb3c9c_jobstep_build_id_job.py │ ├── 10403d977f5f_add_plan_status.py │ ├── 17d61d9621cb_add_command_type.py │ ├── 4114cbbd0573_index_job_status_dat.py │ ├── 4ffb7e1df217_unique_jobphase_job_.py │ ├── 51775a13339d_patch_hash_column.py │ ├── 545e104c5f5a_add_logsource_step_i.py │ ├── 3a3366fb7822_index_testgroup_test.py │ ├── 3c93047c66c8_add_index_to_jobstep_status.py │ ├── 46a612aff7d3_add_snapshot_source_id.py │ ├── 19168fe64c41_unique_constraint_on_command.py │ ├── 256ab638deaa_filecoverage_build_i.py │ ├── 2596f21c6f58_add_project_status.py │ ├── 36cbde703cc0_add_build_priority.py │ ├── 41c7ef24fd4c_add_build_date_decided.py │ ├── 4c3f2a63b7a7_add_plan_avg_build_time.py │ ├── 3d8177efcfe1_add_filecoverage_step_id.py │ ├── 2b7153fe25af_add_repository_status.py │ ├── 2d9df8a3103c_add_logchunk_unique_.py │ ├── 2da2dd72d21c_add_snapshot_date_created.py │ ├── 3961ccb5d884_increase_artifact_name_length.py │ ├── 016f138b2da8_add_job_autogenerated.py │ ├── 15bd4b7e6622_add_filecoverage_unique_constraint.py │ ├── 208224023555_add_repository_backe.py │ ├── 2fa8391277a0_add_jobstep_last_heartbeat.py │ ├── 3f289637f530_remove_unused_models.py │ ├── 520eba1ce36e_remove_author_name_u.py │ ├── 554f414d4c46_set_on_delete_cascad.py │ ├── f8ed1f99eb1_add_projectplan_avg_.py │ ├── 57e4a0553903_add_snapshotimage_status.py │ ├── 8550c7394f4_change_commands_env_to_text.py │ ├── 18c150792a21_add_column_flakyteststat_first_run.py │ ├── 30b9f619c3b2_remove_test_suite.py │ ├── 17c884f6f57c_add_failurereason_column.py │ ├── 1b229c83511d_add_logsource_in_artifact_store.py │ ├── d324e8fc580_add_various_testsuit.py │ ├── 291978a5d5e4_add_revision_date_created_signal.py │ ├── 3aed22af8f4f_add_build_tag.py │ ├── 1db7a1ab95db_fix_filecoverage_pro.py │ ├── 3b22c1663664_change_logsource_constraint.py │ ├── 3042d0ca43bf_index_job_project_id.py │ ├── 4a12e7f0159d_remove_build_repository_id_revision_sha_.py │ ├── 5844fbc9d9e4_add_flakyteststat_double_reruns.py │ ├── 36becb086fcb_add_revision_branche.py │ ├── 3d9067f21201_unique_step_order.py │ ├── 2d82db02b3ef_fill_testgroup_num_l.py │ ├── 3b94584c761e_create_index_in_flakyteststat_for_test_.py │ ├── 382745ed4478_add_jobplan_snapshotimage.py │ ├── 40429d3569cf_add_plan_project_id.py │ ├── cb99fdfb903_add_build_family_id.py │ ├── 460741ff1212_add_project_permissions_column_to_user.py │ ├── 1d806848a73f_unique_buildplan_bui.py │ ├── 45e1cfacfc7d_task_parent_id_is_op.py │ ├── 15f131d88adf_remove_patch_project.py │ ├── 1ad857c78a0d_family_id_build_id.py │ ├── 3df65ebfa27e_remove_patch_label_and_message.py │ ├── 1cb3db431e29_add_build_collection_id.py │ ├── 290a71847cd7_add_jobstep_cluster.py │ ├── 57e24a9f2290_log_build_id_job_id.py │ ├── 3265d2120c82_add_testcase_step_id.py │ ├── 3e85891e90bb_add_jobstep_replacement_id.py │ ├── 12569fada93_require_plan_project_id.py │ ├── 2f3ba1e84a6f_update_source_constr.py │ ├── 3bef10ab5088_add_various_testgrou.py │ ├── 52bcea82b482_remove_patch_url.py │ ├── 3bf1066f4935_add_label_in_testmessage.py │ ├── 47e23df5a7ed_add_testgroup_result.py │ ├── 3cf79ad9bde_add_snapshot_plan_id_to_plan.py │ ├── 14491da59392_add_build_revision_s.py │ ├── fd1a52fe89f_add_various_test_ind.py │ ├── 4640ecd97c82_add_testgroup_num_le.py │ ├── 139e272152de_index_jobphase.py │ ├── 4d5d239d53b4_set_on_delete_cascad.py │ ├── 306fefe51dc6_set_on_delete_cascad.py │ ├── 380d20771802_project_plan.py │ ├── 3be107806e62_make_testartifact_cascade_on_test_delete.py │ ├── 5677ef75c712_add_user.py │ ├── 37e782a55ca6_set_on_delete_cascad.py │ ├── 524b3c27203b_remove_job_author_id.py │ ├── 4768ba7627ac_adjust_command_schema.py │ ├── 5026dbcee21d_test_build_id_job_id.py │ ├── 19910015c867_add_systemoption.py │ ├── 11a7a7f2652f_add_itemstat.py │ ├── 18c045b75331_add_various_build_in.py │ ├── 1b53d33197bf_add_build_cause_and_.py │ ├── 21c9439330f_add_cascade_to_filec.py │ ├── 2d8acbec66df_add_build_target.py │ ├── 4d302aa44bc8_add_additional_revis.py │ ├── 2b1b01d74175_added_adminmessage_table.py │ ├── 32274e97552_build_parent_build_p.py │ ├── 346c011ca77a_optional_parent_revi.py │ ├── 25d63e09ea3b_add_build_job_number.py │ └── 215db24a630a_add_repository_last_.py └── script.py.mako ├── .jshintignore ├── bin ├── worker ├── lint └── shell ├── webapp ├── favicon.ico ├── css │ ├── ABOUT_THE_CSS │ ├── bundle_definition.less │ ├── bundle_definition_custom.less │ ├── bundle_definition_colorblind.less │ ├── bundle_definition_custom_colorblind.less │ ├── wiki_external_link.svg │ └── bootstrap_config.json ├── custom │ └── README ├── pages │ ├── project_page │ │ └── triage_tab.js │ └── examples_page.js └── display │ └── section_header.js ├── docs ├── images │ ├── example.png │ ├── build-email.png │ ├── console-log.png │ └── build-details.png ├── _themes │ └── kr │ │ ├── theme.conf │ │ ├── layout.html │ │ └── relations.html ├── integrations │ └── phabricator.rst └── examples │ └── changes.conf.py ├── docker ├── README └── supervisor-run ├── .coveragerc ├── CONTRIBUTING.rst ├── .sublimelinterrc ├── ci ├── README.txt ├── run_tests.sh └── mypy-run ├── .travis.yml ├── setup.cfg ├── .jshintrc ├── test_requirements.txt ├── bower.json ├── templates └── debug │ └── email.html ├── .gitignore ├── .arclint └── README.rst /.venv: -------------------------------------------------------------------------------- 1 | changes 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/db/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/ext/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/vcs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/web/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /linters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_simple: -------------------------------------------------------------------------------- 1 | testing 2 | -------------------------------------------------------------------------------- /changes/backends/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/db/types/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/debug/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/jobs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/queue/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/reports/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/storage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webapp/dist 2 | -------------------------------------------------------------------------------- /changes/artifacts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/buildsteps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/debug/mail/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/debug/reports/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/expanders/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/jobs/generate_report.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/listeners/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/jobs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/queue/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/storage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/vcs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/web/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/api/validators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/backends/jenkins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # Woo! 2 | -------------------------------------------------------------------------------- /changes/url_converters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/artifacts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/backends/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/listeners/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/api/serializer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/api/validators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/backends/jenkins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changes/backends/jenkins/buildsteps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/changes/api/serializer/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/vendor/requirejs-babel/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /tests/changes/backends/jenkins/buildsteps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "static/vendor" 3 | } 4 | -------------------------------------------------------------------------------- /changes/experimental/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'ar' 2 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /changes/db/funcs/__init__.py: -------------------------------------------------------------------------------- 1 | from .coalesce import * # NOQA 2 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | static/vendor/ 2 | static/js/lib/ 3 | static/js/modules/scalyr.js 4 | -------------------------------------------------------------------------------- /bin/worker: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eux 2 | 3 | celery -A changes.app:celery worker $@ 4 | -------------------------------------------------------------------------------- /static/vendor/classnames/.npmignore: -------------------------------------------------------------------------------- 1 | /.*/ 2 | /.* 3 | /benchmarks/ 4 | /tests/ 5 | -------------------------------------------------------------------------------- /webapp/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/changes/master/webapp/favicon.ico -------------------------------------------------------------------------------- /docs/images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/changes/master/docs/images/example.png -------------------------------------------------------------------------------- /docker/README: -------------------------------------------------------------------------------- 1 | The files in this directory are (mostly) copied into the container and used 2 | there. 3 | -------------------------------------------------------------------------------- /docs/images/build-email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/changes/master/docs/images/build-email.png -------------------------------------------------------------------------------- /docs/images/console-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/changes/master/docs/images/console-log.png -------------------------------------------------------------------------------- /docs/images/build-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/changes/master/docs/images/build-details.png -------------------------------------------------------------------------------- /bin/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PYTHONPATH="$(dirname "$0")/.." exec python -m linters.main "$@" --arc-out 4 | -------------------------------------------------------------------------------- /tests/changes/backends/jenkins/fixtures/GET/queue_item_by_job_id.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 3 | 14 4 | 5 | -------------------------------------------------------------------------------- /tests/changes/backends/jenkins/fixtures/GET/job_list_by_upstream.xml: -------------------------------------------------------------------------------- 1 | 172171 2 | -------------------------------------------------------------------------------- /static/vendor/react/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "main": ["react.js", "react-dom.js"], 4 | "ignore": [] 5 | } 6 | -------------------------------------------------------------------------------- /static/vendor/requirejs/README.md: -------------------------------------------------------------------------------- 1 | # requirejs-bower 2 | 3 | Bower packaging for [RequireJS](http://requirejs.org). 4 | 5 | -------------------------------------------------------------------------------- /changes/testutils/__init__.py: -------------------------------------------------------------------------------- 1 | from .cases import * # NOQA 2 | from .helpers import * # NOQA 3 | from .fixtures import * # NOQA 4 | -------------------------------------------------------------------------------- /tests/changes/backends/jenkins/fixtures/GET/build_item_by_job_id.xml: -------------------------------------------------------------------------------- 1 | 2 | 1 3 | 0 4 | 5 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | changes 4 | tests 5 | omit = 6 | changes/mock.py 7 | 8 | [xml] 9 | output=coverage.xml 10 | -------------------------------------------------------------------------------- /changes/api/serializer/vcs/__init__.py: -------------------------------------------------------------------------------- 1 | from changes.utils.imports import import_submodules 2 | 3 | import_submodules(globals(), __name__, __path__) 4 | -------------------------------------------------------------------------------- /changes/api/serializer/models/__init__.py: -------------------------------------------------------------------------------- 1 | from changes.utils.imports import import_submodules 2 | 3 | import_submodules(globals(), __name__, __path__) 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing to Changes 2 | ----------------------- 3 | 4 | To get started, you'll need to sign Dropbox's CLA: 5 | 6 | https://opensource.dropbox.com/cla/ 7 | -------------------------------------------------------------------------------- /docs/_themes/kr/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | 6 | [options] 7 | touch_icon = 8 | -------------------------------------------------------------------------------- /changes/api/serializer/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * # NOQA 2 | 3 | # register models 4 | from . import models # NOQA 5 | 6 | # register VCS 7 | from . import vcs # NOQA 8 | -------------------------------------------------------------------------------- /changes/buildfailures/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | 4 | class BuildFailure(object): 5 | def get_html_label(self, build): 6 | raise NotImplementedError 7 | -------------------------------------------------------------------------------- /webapp/css/ABOUT_THE_CSS: -------------------------------------------------------------------------------- 1 | The default version of changes is not that pretty. We suggest writing some 2 | custom CSS to style it to look more like your own organization's product / 3 | tooling 4 | -------------------------------------------------------------------------------- /webapp/css/bundle_definition.less: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | @import "color_classes"; 4 | @import "layout_helpers"; 5 | @import "basics"; 6 | @import "display"; 7 | @import "page_specific"; 8 | -------------------------------------------------------------------------------- /.sublimelinterrc: -------------------------------------------------------------------------------- 1 | { 2 | "@python": "2.7", 3 | "linters": { 4 | "pep8": { 5 | "ignore": "E501", 6 | "max-line-length": "100" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ci/README.txt: -------------------------------------------------------------------------------- 1 | These are the scripts used by Changes to setup and run the tests for Changes. 2 | 3 | The intention is that 'setup.sh' is run first, followed by 'run_tests.sh', and 4 | they should be safe to rerun. 5 | -------------------------------------------------------------------------------- /webapp/custom/README: -------------------------------------------------------------------------------- 1 | This directory is for custom css or a js file containing custom content hooks. 2 | Ideally, just put your stuff in version control and check it out here, then 3 | change the config parameters in changes 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | services: 3 | - postgresql 4 | - redis-server 5 | python: 6 | - "2.7" 7 | install: 8 | - make develop 9 | script: 10 | - hg --version 11 | - git --version 12 | - make test 13 | -------------------------------------------------------------------------------- /webapp/css/bundle_definition_custom.less: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "@{custom_css}"; 3 | 4 | @import "color_classes"; 5 | @import "layout_helpers"; 6 | @import "basics"; 7 | @import "display"; 8 | @import "page_specific"; 9 | -------------------------------------------------------------------------------- /static/vendor/classnames/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_install: 3 | - "npm install npm -g" 4 | node_js: 5 | - "0.10" 6 | - "0.12" 7 | - "iojs" 8 | env: 9 | - TEST_SUITE=unit 10 | script: "npm run-script $TEST_SUITE" 11 | -------------------------------------------------------------------------------- /webapp/pages/project_page/triage_tab.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | var TriageTab = React.createClass({ 4 | 5 | render: function() { 6 | return
Coming soon!
; 7 | }, 8 | }) 9 | 10 | export default TriageTab; 11 | -------------------------------------------------------------------------------- /changes/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file acts as a default entry point for app creation. 3 | """ 4 | 5 | from changes.config import create_app, queue 6 | 7 | app = create_app() 8 | 9 | # HACK(dcramer): this allows Celery to detect itself -.- 10 | celery = queue.celery 11 | -------------------------------------------------------------------------------- /webapp/css/bundle_definition_colorblind.less: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | /* red-green colorblind support */ 4 | @green: @blue; 5 | 6 | @import "color_classes"; 7 | @import "layout_helpers"; 8 | @import "basics"; 9 | @import "display"; 10 | @import "page_specific"; 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts=--tb=short 3 | norecursedirs=partials static static-built htmlcov docs node_modules .* *.egg-info{args} env 4 | 5 | [flake8] 6 | ignore = F999,E501,E128,E124,E126,F841,E123 7 | max-line-length = 100 8 | exclude = env,.svn,CVS,.bzr,.hg,.git,__pycache 9 | -------------------------------------------------------------------------------- /tests/changes/api/serializer/models/test_cause.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import serialize 2 | from changes.constants import Cause 3 | 4 | 5 | def test_simple(): 6 | result = serialize(Cause.retry) 7 | assert result['id'] == 'retry' 8 | assert result['name'] == 'Retry' 9 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "strict": true, 3 | "browser": true, 4 | "jquery": true, 5 | "predef": [ 6 | "G_vmlCanvasManager", 7 | "EventSource", 8 | "angular", 9 | "console", 10 | "define", 11 | "require", 12 | "nv", 13 | "d3" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /changes/buildfailures/timeout.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from jinja2 import Markup 4 | 5 | from changes.buildfailures.base import BuildFailure 6 | 7 | 8 | class Timeout(BuildFailure): 9 | def get_html_label(self, build): 10 | return Markup('The build timed out.') 11 | -------------------------------------------------------------------------------- /webapp/css/bundle_definition_custom_colorblind.less: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "@{custom_css}"; 3 | 4 | /* red-green colorblind support */ 5 | @green: @blue; 6 | 7 | @import "color_classes"; 8 | @import "layout_helpers"; 9 | @import "basics"; 10 | @import "display"; 11 | @import "page_specific"; 12 | -------------------------------------------------------------------------------- /bin/shell: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import absolute_import 3 | 4 | from changes.config import create_app 5 | from werkzeug import script 6 | 7 | app = create_app() 8 | app_context = app.app_context() 9 | app_context.push() 10 | 11 | script.make_shell(lambda: {"app": app}, use_ipython=True)() 12 | -------------------------------------------------------------------------------- /static/vendor/uri.js/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "URIjs", 3 | "version": "1.15.2", 4 | "main": "src/URI.js", 5 | "ignore": [ 6 | ".*", 7 | "*.css", 8 | "/*.js", 9 | "/*.html", 10 | "/*.json", 11 | "utils", 12 | "test", 13 | "prettify" 14 | ], 15 | "license": "MIT" 16 | } 17 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | exam>=0.10.2,<0.11.0 2 | flake8>=2.4.0,<2.5.0 3 | loremipsum==1.0.2 4 | mercurial>=2.4 5 | mock>=1.0.1,<1.1.0 6 | pytest>=2.5.0,<2.6.0 7 | pytest-cov>=1.6,<1.7 8 | pytest-timeout>=0.3,<0.4 9 | pytest-xdist>=1.9,<1.10 10 | responses>=0.5.0,<0.6.0 11 | unittest2>=0.5.1,<0.6.0 12 | moto>=0.3.0,<0.4.0 13 | -------------------------------------------------------------------------------- /changes/buildfailures/malformedartifact.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from jinja2 import Markup 4 | 5 | from changes.buildfailures.base import BuildFailure 6 | 7 | 8 | class MalformedArtifact(BuildFailure): 9 | def get_html_label(self, build): 10 | return Markup('An artifact could not be parsed.') 11 | -------------------------------------------------------------------------------- /docker/supervisor-run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin 3 | DAEMON=/usr/bin/supervisord 4 | if [ -f /etc/default/supervisor ] ; then 5 | . /etc/default/supervisor 6 | fi 7 | DAEMON_OPTS="-c /etc/supervisor/supervisord.conf $DAEMON_OPTS" 8 | 9 | exec "$DAEMON" $DAEMON_OPTS --nodaemon 10 | -------------------------------------------------------------------------------- /tests/changes/api/test_client.py: -------------------------------------------------------------------------------- 1 | from changes.api.client import api_client 2 | from changes.testutils import TestCase 3 | 4 | 5 | class APIClientTest(TestCase): 6 | def test_simple(self): 7 | # HACK: relies on existing endpoint 8 | result = api_client.get('/projects/') 9 | assert type(result) == list 10 | -------------------------------------------------------------------------------- /changes/buildfailures/duplicatetestname.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from jinja2 import Markup 4 | 5 | from changes.buildfailures.base import BuildFailure 6 | 7 | 8 | class DuplicateTestName(BuildFailure): 9 | def get_html_label(self, build): 10 | return Markup('A duplicate test name has been found.') 11 | -------------------------------------------------------------------------------- /static/vendor/underscore/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "underscore", 3 | "version": "1.8.3", 4 | "main": "underscore.js", 5 | "keywords": ["util", "functional", "server", "client", "browser"], 6 | "ignore" : ["docs", "test", "*.yml", "CNAME", "index.html", "favicon.ico", "CONTRIBUTING.md", ".*", "component.json", "package.json", "karma.*"] 7 | } 8 | -------------------------------------------------------------------------------- /changes/api/cluster_index.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from changes.api.base import APIView 4 | from changes.models.node import Cluster 5 | 6 | 7 | class ClusterIndexAPIView(APIView): 8 | def get(self): 9 | queryset = Cluster.query.order_by(Cluster.label.asc()) 10 | 11 | return self.paginate(queryset) 12 | -------------------------------------------------------------------------------- /changes/buildfailures/missingmanifestjson.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from jinja2 import Markup 4 | 5 | from changes.buildfailures.base import BuildFailure 6 | 7 | 8 | class MissingManifestJson(BuildFailure): 9 | def get_html_label(self, build): 10 | return Markup('Infrastructure failure (missing manifest file)') 11 | -------------------------------------------------------------------------------- /changes/buildfailures/missingtests.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from jinja2 import Markup 4 | 5 | from changes.buildfailures.base import BuildFailure 6 | 7 | 8 | class MissingTests(BuildFailure): 9 | def get_html_label(self, build): 10 | return Markup('Tests were expected for all results, but some or all were missing.') 11 | -------------------------------------------------------------------------------- /changes/db/funcs/coalesce.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.compiler import compiles 2 | from sqlalchemy.sql.expression import FunctionElement 3 | 4 | 5 | class coalesce(FunctionElement): 6 | name = 'coalesce' 7 | 8 | 9 | @compiles(coalesce, 'postgresql') 10 | def compile(element, compiler, **kw): 11 | return "coalesce(%s)" % compiler.process(element.clauses) 12 | -------------------------------------------------------------------------------- /ci/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | source ~/env/bin/activate 4 | # webapp/entry.js must be ES5-compatible; we don't currently preprocess it. 5 | node_modules/.bin/eslint --no-eslintrc --parser-options=ecmaVersion:5 -f junit webapp/entry.js > entry_eslint.junit.xml 6 | node_modules/.bin/eslint --plugin react --quiet -f junit webapp/ > webapp_eslint.junit.xml 7 | make test-full 8 | -------------------------------------------------------------------------------- /docs/integrations/phabricator.rst: -------------------------------------------------------------------------------- 1 | Phabricator Integration 2 | ======================= 3 | 4 | While Changes provides no direct integration with Phabricator, a build step is available as an external library: 5 | 6 | https://github.com/dropbox/phabricator-changes 7 | 8 | The build step allows you to wire up your Phabricator install to submit commits and diffs to Changes as builds. 9 | -------------------------------------------------------------------------------- /changes/buildfailures/missingtargets.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from jinja2 import Markup 4 | 5 | from changes.buildfailures.base import BuildFailure 6 | 7 | 8 | class MissingTargets(BuildFailure): 9 | def get_html_label(self, build): 10 | return Markup('Some Bazel targets did not produce a test.xml artifact, most likely due to a build failure.') 11 | -------------------------------------------------------------------------------- /changes/buildfailures/malformedmanifestjson.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from jinja2 import Markup 4 | 5 | from changes.buildfailures.base import BuildFailure 6 | 7 | 8 | class MalformedManifestJson(BuildFailure): 9 | def get_html_label(self, build): 10 | return Markup('Infrastructure failure (manifest.json file was malformed or had incorrect JobStep id)') 11 | -------------------------------------------------------------------------------- /changes/api/cached_snapshot_cluster_details.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from changes.api.base import APIView 4 | import changes.lib.snapshot_garbage_collection as gc 5 | 6 | 7 | class CachedSnapshotClusterDetailsAPIView(APIView): 8 | def get(self, cluster): 9 | images = gc.get_cached_snapshot_images(cluster) 10 | return self.respond([image.id for image in images]) 11 | -------------------------------------------------------------------------------- /changes/api/serializer/models/latest_green_build.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.latest_green_build import LatestGreenBuild 3 | 4 | 5 | @register(LatestGreenBuild) 6 | class LatestGreenBuildCrumbler(Crumbler): 7 | def crumble(self, item, attrs): 8 | return { 9 | 'branch': item.branch, 10 | 'build': item.build, 11 | } 12 | -------------------------------------------------------------------------------- /changes/api/serializer/models/node.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.node import Node 3 | 4 | 5 | @register(Node) 6 | class NodeCrumbler(Crumbler): 7 | def crumble(self, instance, attrs): 8 | return { 9 | 'id': instance.id.hex, 10 | 'name': instance.label, 11 | 'dateCreated': instance.date_created, 12 | } 13 | -------------------------------------------------------------------------------- /static/vendor/requirejs/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "requirejs", 3 | "version": "2.1.22", 4 | "ignore": [], 5 | "homepage": "http://requirejs.org", 6 | "authors": [ 7 | "jrburke.com" 8 | ], 9 | "description": "A file and module loader for JavaScript", 10 | "main": "require.js", 11 | "keywords": [ 12 | "AMD" 13 | ], 14 | "license": [ 15 | "BSD-3-Clause", 16 | "MIT" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /changes/api/serializer/models/cluster.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.node import Cluster 3 | 4 | 5 | @register(Cluster) 6 | class ClusterCrumbler(Crumbler): 7 | def crumble(self, instance, attrs): 8 | return { 9 | 'id': instance.id.hex, 10 | 'name': instance.label, 11 | 'dateCreated': instance.date_created, 12 | } 13 | -------------------------------------------------------------------------------- /changes/utils/times.py: -------------------------------------------------------------------------------- 1 | def duration(value): 2 | ONE_SECOND = 1000 3 | ONE_MINUTE = ONE_SECOND * 60 4 | 5 | if not value: 6 | return '0 s' 7 | 8 | abs_value = abs(value) 9 | 10 | if abs_value < 3 * ONE_SECOND: 11 | return '%d ms' % (value,) 12 | elif abs_value < 5 * ONE_MINUTE: 13 | return '%d s' % (value / ONE_SECOND,) 14 | else: 15 | return '%d m' % (value / ONE_MINUTE,) 16 | -------------------------------------------------------------------------------- /tests/changes/api/test_node_details.py: -------------------------------------------------------------------------------- 1 | from changes.testutils import APITestCase 2 | 3 | 4 | class NodeDetailsTest(APITestCase): 5 | def test_simple(self): 6 | node = self.create_node() 7 | path = '/api/0/nodes/{0}/'.format(node.id.hex) 8 | 9 | resp = self.client.get(path) 10 | assert resp.status_code == 200 11 | data = self.unserialize(resp) 12 | assert data['id'] == node.id.hex 13 | -------------------------------------------------------------------------------- /changes/api/change_details.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import joinedload 2 | 3 | from changes.api.base import APIView 4 | from changes.models.change import Change 5 | 6 | 7 | class ChangeDetailsAPIView(APIView): 8 | def get(self, change_id): 9 | change = Change.query.options( 10 | joinedload(Change.project), 11 | joinedload(Change.author), 12 | ).get(change_id) 13 | 14 | return self.respond(change) 15 | -------------------------------------------------------------------------------- /changes/utils/dirs.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division 2 | 3 | import os 4 | 5 | from changes.constants import PROJECT_ROOT 6 | 7 | 8 | def enforce_is_subdir(path, root=PROJECT_ROOT): 9 | """ 10 | Ensure that a particular path is within a subdirectory of changes. 11 | Prevents directory traversal attacks 12 | """ 13 | if not os.path.abspath(path).startswith(root): 14 | raise RuntimeError('this path is not safe!') 15 | -------------------------------------------------------------------------------- /changes/api/validators/basic.py: -------------------------------------------------------------------------------- 1 | from typing import Callable # NOQA 2 | 3 | 4 | def bounded_integer(lower, upper): 5 | """Accepts an integer in [lower, upper].""" 6 | # type: (int, int) -> Callable[str, int] 7 | def parse(s): 8 | # type: (str) -> int 9 | iv = int(s) 10 | if iv < lower or iv > upper: 11 | raise ValueError("{} is not in [{}, {}]".format(iv, lower, upper)) 12 | return iv 13 | return parse 14 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "changes", 3 | "version": "0.1.0", 4 | "ignore": [ 5 | "**/.*", 6 | "node_modules", 7 | "static/vendor", 8 | "tests" 9 | ], 10 | "devDependencies": { 11 | "classnames": "~2.2.0", 12 | "moment": "~2.1.0", 13 | "react": "~15.0", 14 | "react-bootstrap": "~0.29", 15 | "requirejs": "~2.1.11", 16 | "requirejs-babel": "0.0.6", 17 | "underscore": "~1.8.3", 18 | "uri.js": "~1.15.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /changes/buildfailures/missingartifact.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from jinja2 import Markup 4 | 5 | from changes.buildfailures.base import BuildFailure 6 | 7 | 8 | class MissingArtifact(BuildFailure): 9 | def get_html_label(self, build): 10 | # TODO(dcramer): we need arbitrary data with build failures so this can 11 | # say *what* artifact 12 | return Markup('A critical artifact was expected, but was not collected.') 13 | -------------------------------------------------------------------------------- /tests/changes/api/test_cluster_details.py: -------------------------------------------------------------------------------- 1 | from changes.testutils import APITestCase 2 | 3 | 4 | class ClusterIndexTest(APITestCase): 5 | def test_simple(self): 6 | cluster_1 = self.create_cluster(label='bar') 7 | path = '/api/0/clusters/{0}/'.format(cluster_1.id.hex) 8 | 9 | resp = self.client.get(path) 10 | assert resp.status_code == 200 11 | data = self.unserialize(resp) 12 | assert data['id'] == cluster_1.id.hex 13 | -------------------------------------------------------------------------------- /changes/api/serializer/models/comment.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.comment import Comment 3 | 4 | 5 | @register(Comment) 6 | class CommentCrumbler(Crumbler): 7 | def crumble(self, instance, attrs): 8 | return { 9 | 'id': instance.id.hex, 10 | 'user': instance.user, 11 | 'text': instance.text, 12 | 'dateCreated': instance.date_created.isoformat(), 13 | } 14 | -------------------------------------------------------------------------------- /static/vendor/requirejs-babel/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "requirejs-babel", 3 | "version": "0.0.6", 4 | "authors": [ 5 | "Michael " 6 | ], 7 | "description": "An AMD loader plugin for Babel", 8 | "main": "es6.js", 9 | "moduleType": [ 10 | "amd" 11 | ], 12 | "keywords": [ 13 | "es6", 14 | "esnext", 15 | "babel" 16 | ], 17 | "license": "MIT", 18 | "ignore": [ 19 | "demo", 20 | "package.json" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /migrations/versions/4134b4818694_remove_index_build_p.py: -------------------------------------------------------------------------------- 1 | """Remove index Build.project_id 2 | 3 | Revision ID: 4134b4818694 4 | Revises: f2c8d15416b 5 | Create Date: 2013-12-03 16:18:03.550867 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4134b4818694' 11 | down_revision = 'f2c8d15416b' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.drop_index('idx_build_project_id', 'build') 18 | 19 | 20 | def downgrade(): 21 | pass 22 | -------------------------------------------------------------------------------- /static/vendor/moment/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moment", 3 | "version": "2.1.0", 4 | "scripts": ["moment.js"], 5 | "main": "moment.js", 6 | "description": "Parse, validate, manipulate, and display dates in javascript.", 7 | "ignore": [ 8 | ".gitignore", 9 | ".travis.yml", 10 | "composer.json", 11 | "CONTRIBUTING.md", 12 | "ender.js", 13 | "Gruntfile.js", 14 | "package.js", 15 | "package.json", 16 | "test", 17 | "tasks" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /static/vendor/moment/lang/readme.md: -------------------------------------------------------------------------------- 1 | # Using language files in the browser 2 | 3 | The files in this directory are optimized for use with Node.js, hence the `require('../moment')` calls at the beginning of each file. 4 | 5 | If you are looking to use these files in the browser, the minified version of these files in `/min/lang/` are a better fit. We do some trickery to make each language file work with or without requirejs. If you are interested in the details, check out `/tasks/minify-lang.js`. 6 | -------------------------------------------------------------------------------- /changes/api/serializer/models/adminmessage.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.adminmessage import AdminMessage 3 | 4 | 5 | @register(AdminMessage) 6 | class AdminMessageCrumbler(Crumbler): 7 | def crumble(self, instance, attrs): 8 | return { 9 | 'id': instance.id.hex, 10 | 'user': instance.user, 11 | 'message': instance.message, 12 | 'dateCreated': instance.date_created, 13 | } 14 | -------------------------------------------------------------------------------- /migrations/versions/fe743605e1a_remove_repositoryoption.py: -------------------------------------------------------------------------------- 1 | """Remove RepositoryOption 2 | 3 | Revision ID: fe743605e1a 4 | Revises: 10403d977f5f 5 | Create Date: 2014-09-17 15:17:09.925681 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = 'fe743605e1a' 11 | down_revision = '10403d977f5f' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.drop_table('repositoryoption') 18 | 19 | 20 | def downgrade(): 21 | raise NotImplementedError 22 | -------------------------------------------------------------------------------- /changes/backends/base.py: -------------------------------------------------------------------------------- 1 | class UnrecoverableException(Exception): 2 | pass 3 | 4 | 5 | class BaseBackend(object): 6 | def __init__(self, app): 7 | self.app = app 8 | 9 | def create_job(self, job): 10 | raise NotImplementedError 11 | 12 | def sync_job(self, job): 13 | raise NotImplementedError 14 | 15 | def sync_step(self, step): 16 | raise NotImplementedError 17 | 18 | def cancel_step(self, step): 19 | raise NotImplementedError 20 | -------------------------------------------------------------------------------- /changes/api/serializer/models/buildmessage.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.buildmessage import BuildMessage 3 | 4 | 5 | @register(BuildMessage) 6 | class BuildMessageCrumbler(Crumbler): 7 | def crumble(self, instance, attrs): 8 | return { 9 | 'id': instance.id.hex, 10 | 'build': {'id': instance.build_id.hex}, 11 | 'text': instance.text, 12 | 'dateCreated': instance.date_created, 13 | } 14 | -------------------------------------------------------------------------------- /tests/changes/api/test_change_details.py: -------------------------------------------------------------------------------- 1 | from changes.testutils import APITestCase 2 | 3 | 4 | class ChangeDetailsTest(APITestCase): 5 | def test_simple(self): 6 | project = self.create_project() 7 | change = self.create_change(project) 8 | 9 | path = '/api/0/changes/{0}/'.format(change.id.hex) 10 | 11 | resp = self.client.get(path) 12 | assert resp.status_code == 200 13 | data = self.unserialize(resp) 14 | assert data['id'] == change.id.hex 15 | -------------------------------------------------------------------------------- /changes/api/serializer/models/testartifact.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.testartifact import TestArtifact 3 | 4 | 5 | @register(TestArtifact) 6 | class TestArtifactCrumbler(Crumbler): 7 | def crumble(self, instance, attrs): 8 | return { 9 | 'id': instance.id.hex, 10 | 'name': instance.name, 11 | 'url': instance.file.url_for() if instance.file else None, 12 | 'type': instance.type 13 | } 14 | -------------------------------------------------------------------------------- /templates/debug/email.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Subject: {{ subject }}
7 | Recipients: {{ recipients }}

8 | 9 | Html email: 10 |
11 | {{ html_content }} 12 |
13 |
14 | Text email: 15 |
16 |
{{ text_content }}
17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /migrations/versions/5aad6f742b77_parent_revision_sha_.py: -------------------------------------------------------------------------------- 1 | """parent_revision_sha => revision_sha 2 | 3 | Revision ID: 5aad6f742b77 4 | Revises: 2d82db02b3ef 5 | Create Date: 2013-11-11 17:05:31.671178 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '5aad6f742b77' 11 | down_revision = '2d82db02b3ef' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute("update build set revision_sha = parent_revision_sha") 18 | 19 | 20 | def downgrade(): 21 | pass 22 | -------------------------------------------------------------------------------- /migrations/versions/4f955feb6d07_rename_build_job.py: -------------------------------------------------------------------------------- 1 | """Rename Build -> Job 2 | 3 | Revision ID: 4f955feb6d07 4 | Revises: 554f414d4c46 5 | Create Date: 2013-12-25 23:17:26.666301 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4f955feb6d07' 11 | down_revision = '554f414d4c46' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute('ALTER TABLE build RENAME TO job') 18 | 19 | 20 | def downgrade(): 21 | op.execute('ALTER TABLE job RENAME TO build') 22 | -------------------------------------------------------------------------------- /changes/api/node_details.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from changes.api.base import APIView 4 | from changes.models.node import Node 5 | 6 | 7 | class NodeDetailsAPIView(APIView): 8 | def get(self, node_id): 9 | node = Node.query.get(node_id) 10 | if node is None: 11 | return '', 404 12 | 13 | context = self.serialize(node) 14 | context['clusters'] = self.serialize(list(node.clusters)) 15 | 16 | return self.respond(context, serialize=False) 17 | -------------------------------------------------------------------------------- /changes/url_converters/uuid.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from uuid import UUID 4 | from werkzeug.routing import BaseConverter, ValidationError 5 | 6 | 7 | class UUIDConverter(BaseConverter): 8 | """ 9 | UUID converter for the Werkzeug routing system. 10 | """ 11 | def to_python(self, value): 12 | try: 13 | return UUID(value) 14 | except ValueError: 15 | raise ValidationError 16 | 17 | def to_url(self, value): 18 | return str(value) 19 | -------------------------------------------------------------------------------- /migrations/versions/f2c8d15416b_index_build_project_.py: -------------------------------------------------------------------------------- 1 | """Index Build.project_id,date_created 2 | 3 | Revision ID: f2c8d15416b 4 | Revises: 152c9c780e 5 | Create Date: 2013-12-03 15:54:09.488066 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = 'f2c8d15416b' 11 | down_revision = '152c9c780e' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_index('idx_build_project_date', 'build', ['project_id', 'date_created']) 18 | 19 | 20 | def downgrade(): 21 | pass 22 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = ${repr(up_revision)} 11 | down_revision = ${repr(down_revision)} 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | ${imports if imports else ""} 16 | 17 | def upgrade(): 18 | ${upgrades if upgrades else "pass"} 19 | 20 | 21 | def downgrade(): 22 | ${downgrades if downgrades else "pass"} 23 | -------------------------------------------------------------------------------- /migrations/versions/105d4dd82a0a_date_indexes_on_test.py: -------------------------------------------------------------------------------- 1 | """Date indexes on TestGroup 2 | 3 | Revision ID: 105d4dd82a0a 4 | Revises: 2d9df8a3103c 5 | Create Date: 2013-11-19 12:24:02.607191 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '105d4dd82a0a' 11 | down_revision = '2d9df8a3103c' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_index('idx_testgroup_project_date', 'testgroup', ['project_id', 'date_created']) 18 | 19 | 20 | def downgrade(): 21 | pass 22 | -------------------------------------------------------------------------------- /changes/api/build_coverage.py: -------------------------------------------------------------------------------- 1 | from changes.api.base import APIView 2 | 3 | from changes.lib.coverage import get_coverage_by_build_id, merged_coverage_data 4 | 5 | from changes.models.build import Build 6 | 7 | 8 | class BuildTestCoverageAPIView(APIView): 9 | def get(self, build_id): 10 | build = Build.query.get(build_id) 11 | if build is None: 12 | return '', 404 13 | 14 | coverage = merged_coverage_data(get_coverage_by_build_id(build.id)) 15 | 16 | return self.respond(coverage) 17 | -------------------------------------------------------------------------------- /changes/api/serializer/models/bazeltargetmessage.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.bazeltargetmessage import BazelTargetMessage 3 | 4 | 5 | @register(BazelTargetMessage) 6 | class BazelTargetMessageCrumbler(Crumbler): 7 | def crumble(self, instance, attrs): 8 | return { 9 | 'id': instance.id.hex, 10 | 'target': {'id': instance.target_id.hex}, 11 | 'text': instance.text, 12 | 'dateCreated': instance.date_created, 13 | } 14 | -------------------------------------------------------------------------------- /migrations/versions/2622a69cd25a_unique_node_label.py: -------------------------------------------------------------------------------- 1 | """Unique Node.label 2 | 3 | Revision ID: 2622a69cd25a 4 | Revises: 2c7cbd9b7e54 5 | Create Date: 2013-12-18 12:42:30.583085 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2622a69cd25a' 11 | down_revision = '2c7cbd9b7e54' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_unique_constraint('unq_node_label', 'node', ['label']) 18 | 19 | 20 | def downgrade(): 21 | op.drop_constraint('unq_node_label', 'node') 22 | -------------------------------------------------------------------------------- /migrations/versions/4901f27ade8e_fill_target.py: -------------------------------------------------------------------------------- 1 | """Fill target 2 | 3 | Revision ID: 4901f27ade8e 4 | Revises: 52bcea82b482 5 | Create Date: 2013-11-11 17:25:38.930307 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4901f27ade8e' 11 | down_revision = '52bcea82b482' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute("update build set target = substr(revision_sha, 0, 12) where target is null and revision_sha is not null") 18 | 19 | 20 | def downgrade(): 21 | pass 22 | -------------------------------------------------------------------------------- /static/vendor/react-bootstrap/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-bootstrap", 3 | "version": "0.29.3", 4 | "homepage": "http://react-bootstrap.github.io/", 5 | "author": "Stephen J. Collings ", 6 | "license": "MIT", 7 | "main": [ 8 | "react-bootstrap.js" 9 | ], 10 | "keywords": [ 11 | "react", 12 | "ecosystem-react", 13 | "react-component", 14 | "bootstrap" 15 | ], 16 | "ignore": [ 17 | "**/.*" 18 | ], 19 | "dependencies": { 20 | "react": ">=0.14.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /static/vendor/react/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "main": [ 4 | "react.js", 5 | "react-dom.js" 6 | ], 7 | "ignore": [], 8 | "homepage": "https://github.com/facebook/react-bower", 9 | "version": "15.0.2", 10 | "_release": "15.0.2", 11 | "_resolution": { 12 | "type": "version", 13 | "tag": "v15.0.2", 14 | "commit": "624b4f21e8bf11b7a102cd0855553d1a4279da3f" 15 | }, 16 | "_source": "https://github.com/facebook/react-bower.git", 17 | "_target": "~15.0", 18 | "_originalSource": "react" 19 | } -------------------------------------------------------------------------------- /docs/examples/changes.conf.py: -------------------------------------------------------------------------------- 1 | # ~/.changes/changes.conf.py 2 | WEB_BASE_URI = 'http://localhost:5000' 3 | INTERNAL_BASE_URI = 'http://localhost:5000' 4 | SERVER_NAME = 'localhost:5000' 5 | 6 | REPO_ROOT = '/tmp' 7 | 8 | # You can obtain these values via the Google Developers Console: 9 | # https://console.developers.google.com/ 10 | # Example 'Authorized JavaScript Origins': http://localhost:5000 11 | # Example 'Authorized Redirect URIs': http://localhost:5000/auth/complete/ 12 | GOOGLE_CLIENT_ID = None 13 | GOOGLE_CLIENT_SECRET = None 14 | -------------------------------------------------------------------------------- /migrations/versions/5508859bed73_index_source_patch_i.py: -------------------------------------------------------------------------------- 1 | """Index Source.patch_id 2 | 3 | Revision ID: 5508859bed73 4 | Revises: 26f665189ca0 5 | Create Date: 2014-01-06 11:14:19.109932 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '5508859bed73' 11 | down_revision = '26f665189ca0' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_index('idx_source_patch_id', 'source', ['patch_id']) 18 | 19 | 20 | def downgrade(): 21 | op.drop_index('idx_source_patch_id', 'source') 22 | -------------------------------------------------------------------------------- /changes/api/serializer/models/artifact.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.artifact import Artifact 3 | 4 | 5 | @register(Artifact) 6 | class ArtifactCrumbler(Crumbler): 7 | def crumble(self, instance, attrs): 8 | return { 9 | 'id': instance.id.hex, 10 | 'jobstep': instance.step, 11 | 'name': instance.name, 12 | 'url': instance.file.url_for() if instance.file else None, 13 | 'dateCreated': instance.date_created, 14 | } 15 | -------------------------------------------------------------------------------- /migrations/versions/34157be0e8a2_index_patch_project_.py: -------------------------------------------------------------------------------- 1 | """Index Patch.project_id 2 | 3 | Revision ID: 34157be0e8a2 4 | Revises: f8ed1f99eb1 5 | Create Date: 2014-01-02 21:59:37.076886 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '34157be0e8a2' 11 | down_revision = 'f8ed1f99eb1' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_index('idx_patch_project_id', 'patch', ['project_id']) 18 | 19 | 20 | def downgrade(): 21 | op.drop_index('idx_patch_project_id', 'patch') 22 | -------------------------------------------------------------------------------- /migrations/versions/42e9d35a4098_rename_buildstep_job.py: -------------------------------------------------------------------------------- 1 | """Rename BuildStep -> JobStep 2 | 3 | Revision ID: 42e9d35a4098 4 | Revises: 516f04a1a754 5 | Create Date: 2013-12-25 23:44:30.776610 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '42e9d35a4098' 11 | down_revision = '516f04a1a754' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute('ALTER TABLE buildstep RENAME TO jobstep') 18 | 19 | 20 | def downgrade(): 21 | op.execute('ALTER TABLE jobstep RENAME TO buildstep') 22 | -------------------------------------------------------------------------------- /migrations/versions/545ba163595_rename_buildfamily_b.py: -------------------------------------------------------------------------------- 1 | """Rename BuildFamily => Build 2 | 3 | Revision ID: 545ba163595 4 | Revises: 256ab638deaa 5 | Create Date: 2013-12-26 01:51:58.807080 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '545ba163595' 11 | down_revision = '256ab638deaa' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute('ALTER TABLE buildfamily RENAME TO build') 18 | 19 | 20 | def downgrade(): 21 | op.execute('ALTER TABLE build RENAME TO buildfamily') 22 | -------------------------------------------------------------------------------- /migrations/versions/6483270c001_rename_buildplan_job.py: -------------------------------------------------------------------------------- 1 | """Rename BuildPlan -> JobPlan 2 | 3 | Revision ID: 6483270c001 4 | Revises: 4f955feb6d07 5 | Create Date: 2013-12-25 23:37:07.896471 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '6483270c001' 11 | down_revision = '4f955feb6d07' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute('ALTER TABLE buildplan RENAME TO jobplan') 18 | 19 | 20 | def downgrade(): 21 | op.execute('ALTER TABLE jobplan RENAME TO buildplan') 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .arcconfig 2 | .arc_lib 3 | .coverage 4 | .DS_Store 5 | .idea 6 | *.iml 7 | *.db 8 | *.egg-info 9 | *.pyc 10 | *.swp 11 | *.swo 12 | npm-debug.log 13 | /env 14 | /*.xml 15 | /docs/_build/ 16 | /htmlcov 17 | /dist 18 | /node_modules/babel-eslint/* 19 | /node_modules/eslint/* 20 | /node_modules/eslint-plugin-react/* 21 | /static/js/generated 22 | /static-built 23 | /webapp/custom/* 24 | !/webapp/custom/README 25 | /webapp/dist 26 | /webapp/css/bundled.css 27 | /webapp/css/bundled_with_custom.css 28 | webapp/.webassets-cache 29 | .vagrant 30 | -------------------------------------------------------------------------------- /migrations/versions/516f04a1a754_rename_buildphase_jo.py: -------------------------------------------------------------------------------- 1 | """Rename BuildPhase -> JobPhase 2 | 3 | Revision ID: 516f04a1a754 4 | Revises: 6483270c001 5 | Create Date: 2013-12-25 23:40:42.892745 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '516f04a1a754' 11 | down_revision = '6483270c001' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute('ALTER TABLE buildphase RENAME TO jobphase') 18 | 19 | 20 | def downgrade(): 21 | op.execute('ALTER TABLE jobphase RENAME TO buildphase') 22 | -------------------------------------------------------------------------------- /migrations/versions/97786e74292_add_task_data.py: -------------------------------------------------------------------------------- 1 | """Add Task.data 2 | 3 | Revision ID: 97786e74292 4 | Revises: 4ae598236ef1 5 | Create Date: 2014-01-21 17:40:42.013002 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '97786e74292' 11 | down_revision = '4ae598236ef1' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('task', sa.Column('data', sa.JSONEncodedDict(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('task', 'data') 23 | -------------------------------------------------------------------------------- /changes/api/patch_details.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from flask import request, Response 4 | 5 | from changes.api.base import APIView 6 | from changes.models.patch import Patch 7 | 8 | 9 | class PatchDetailsAPIView(APIView): 10 | def get(self, patch_id): 11 | patch = Patch.query.get(patch_id) 12 | if patch is None: 13 | return '', 404 14 | 15 | if request.args.get('raw'): 16 | return Response(patch.diff, mimetype='text/plain') 17 | 18 | return self.respond(patch) 19 | -------------------------------------------------------------------------------- /changes/api/serializer/models/revisionresult.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from changes.api.serializer import Crumbler, register 4 | from changes.models.revisionresult import RevisionResult 5 | 6 | 7 | @register(RevisionResult) 8 | class RevisionResultCrumbler(Crumbler): 9 | def crumble(self, instance, attrs): 10 | return { 11 | 'id': instance.id.hex, 12 | 'revisionSha': instance.revision_sha, 13 | 'build': instance.build, 14 | 'result': instance.result, 15 | } 16 | -------------------------------------------------------------------------------- /changes/api/serializer/models/snapshotimage.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.snapshot import SnapshotImage 3 | 4 | 5 | @register(SnapshotImage) 6 | class SnapshotImageCrumbler(Crumbler): 7 | def crumble(self, instance, attrs): 8 | return { 9 | 'id': instance.id.hex, 10 | 'status': instance.status, 11 | 'dateCreated': instance.date_created, 12 | 'plan': instance.plan, 13 | 'snapshot': {'id': instance.snapshot.id.hex}, 14 | } 15 | -------------------------------------------------------------------------------- /migrations/versions/4ae598236ef1_unused_indexes_on_jo.py: -------------------------------------------------------------------------------- 1 | """Unused indexes on Job 2 | 3 | Revision ID: 4ae598236ef1 4 | Revises: 4ffb7e1df217 5 | Create Date: 2014-01-21 14:19:27.134472 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4ae598236ef1' 11 | down_revision = '4ffb7e1df217' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.drop_index('idx_build_source_id', 'job') 18 | op.drop_index('idx_build_family_id', 'job') 19 | 20 | 21 | def downgrade(): 22 | raise NotImplementedError 23 | -------------------------------------------------------------------------------- /changes/api/serializer/models/logchunk.py: -------------------------------------------------------------------------------- 1 | 2 | from changes.api.serializer import Crumbler, register 3 | from changes.models.log import LogChunk 4 | 5 | 6 | @register(LogChunk) 7 | class LogChunkCrumbler(Crumbler): 8 | def crumble(self, instance, attrs): 9 | return { 10 | 'id': instance.id.hex, 11 | 'source': { 12 | 'id': instance.source.id.hex, 13 | }, 14 | 'text': instance.text, 15 | 'offset': instance.offset, 16 | 'size': instance.size, 17 | } 18 | -------------------------------------------------------------------------------- /migrations/versions/181adec926e2_add_status_index_to_task.py: -------------------------------------------------------------------------------- 1 | """Add status index to task 2 | 3 | Revision ID: 181adec926e2 4 | Revises: 43397e521791 5 | Create Date: 2016-10-03 17:41:44.038137 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '181adec926e2' 11 | down_revision = '43397e521791' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_index('idx_task_status', 'task', ['status'], unique=False) 18 | 19 | 20 | def downgrade(): 21 | op.drop_index('id_task_status', table_name='task') 22 | -------------------------------------------------------------------------------- /migrations/versions/21b7c3b2ce88_index_build_project_.py: -------------------------------------------------------------------------------- 1 | """Index Build.project_id,patch_id,date_created 2 | 3 | Revision ID: 21b7c3b2ce88 4 | Revises: 4134b4818694 5 | Create Date: 2013-12-03 16:19:11.794912 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '21b7c3b2ce88' 11 | down_revision = '4134b4818694' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_index('idx_build_project_patch_date', 'build', ['project_id', 'patch_id', 'date_created']) 18 | 19 | 20 | def downgrade(): 21 | pass 22 | -------------------------------------------------------------------------------- /migrations/versions/36d7c98ddfee_drop_projectplan_table.py: -------------------------------------------------------------------------------- 1 | """Drop ProjectPlan table 2 | 3 | Revision ID: 36d7c98ddfee 4 | Revises: 12569fada93 5 | Create Date: 2014-10-14 11:25:48.151275 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '36d7c98ddfee' 11 | down_revision = '12569fada93' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | ### commands auto generated by Alembic - please adjust! ### 18 | op.drop_table('project_plan') 19 | 20 | 21 | def downgrade(): 22 | raise NotImplementedError 23 | -------------------------------------------------------------------------------- /changes/storage/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | 4 | class FileStorage(object): 5 | def __init__(self, path=''): 6 | self.path = path 7 | 8 | def save(self, filename, fp, content_type=None, path=None): 9 | raise NotImplementedError 10 | 11 | def url_for(self, filename, expire=300): 12 | raise NotImplementedError 13 | 14 | def get_file(self, filename, offset=None, length=None): 15 | raise NotImplementedError 16 | 17 | def get_size(self, filename): 18 | raise NotImplementedError 19 | -------------------------------------------------------------------------------- /migrations/versions/21a9d1ebe15c_add_jobplan_data.py: -------------------------------------------------------------------------------- 1 | """Add JobPlan.data 2 | 3 | Revision ID: 21a9d1ebe15c 4 | Revises: 19b8969073ab 5 | Create Date: 2014-07-15 17:30:50.899914 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '21a9d1ebe15c' 11 | down_revision = '19b8969073ab' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('jobplan', sa.Column('data', sa.JSONEncodedDict(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('jobplan', 'data') 23 | -------------------------------------------------------------------------------- /migrations/versions/26c0affcb18a_add_jobstep_data.py: -------------------------------------------------------------------------------- 1 | """Add JobStep.data 2 | 3 | Revision ID: 26c0affcb18a 4 | Revises: 4114cbbd0573 5 | Create Date: 2014-01-07 16:44:48.524027 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '26c0affcb18a' 11 | down_revision = '4114cbbd0573' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('jobstep', sa.Column('data', sa.JSONEncodedDict(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('jobstep', 'data') 23 | -------------------------------------------------------------------------------- /migrations/versions/315e787e94bf_add_source_data.py: -------------------------------------------------------------------------------- 1 | """Add Source.data 2 | 3 | Revision ID: 315e787e94bf 4 | Revises: 3f289637f530 5 | Create Date: 2014-05-06 18:25:11.223951 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '315e787e94bf' 11 | down_revision = '3f289637f530' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('source', sa.Column('data', sa.JSONEncodedDict(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('source', 'data') 23 | -------------------------------------------------------------------------------- /migrations/versions/4ba1dd8c3080_add_index_for_projec.py: -------------------------------------------------------------------------------- 1 | """Add index for project test list 2 | 3 | Revision ID: 4ba1dd8c3080 4 | Revises: 2c380be0a31e 5 | Create Date: 2014-04-14 11:08:05.598439 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4ba1dd8c3080' 11 | down_revision = '2c380be0a31e' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_index('idx_test_project_key', 'test', ['project_id', 'label_sha']) 18 | 19 | 20 | def downgrade(): 21 | op.drop_index('idx_test_project_key', 'test') 22 | -------------------------------------------------------------------------------- /migrations/versions/4d235d421320_add_artifact_file.py: -------------------------------------------------------------------------------- 1 | """Add Artifact.file 2 | 3 | Revision ID: 4d235d421320 4 | Revises: 19910015c867 5 | Create Date: 2014-07-14 17:24:54.764351 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4d235d421320' 11 | down_revision = '19910015c867' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('artifact', sa.Column('file', sa.FileStorage(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('artifact', 'file') 23 | -------------------------------------------------------------------------------- /migrations/versions/bb141e41aab_add_rerun_count_to_t.py: -------------------------------------------------------------------------------- 1 | """add rerun count to tests 2 | 3 | Revision ID: bb141e41aab 4 | Revises: f8f72eecc7f 5 | Create Date: 2014-03-28 13:57:17.364930 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = 'bb141e41aab' 11 | down_revision = 'f8f72eecc7f' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column(u'test', sa.Column('reruns', sa.INTEGER(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column(u'test', 'reruns') 23 | -------------------------------------------------------------------------------- /migrations/versions/f8f72eecc7f_remove_idx_testgroup.py: -------------------------------------------------------------------------------- 1 | """Remove idx_testgroup_project_id 2 | 3 | Revision ID: f8f72eecc7f 4 | Revises: 1db7a1ab95db 5 | Create Date: 2014-03-27 12:12:15.548413 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = 'f8f72eecc7f' 11 | down_revision = '1db7a1ab95db' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.drop_index('idx_testgroup_project_id', 'testgroup') 18 | 19 | 20 | def downgrade(): 21 | op.create_index('idx_testgroup_project_id', 'testgroup', ['project_id']) 22 | -------------------------------------------------------------------------------- /tests/changes/api/test_node_index.py: -------------------------------------------------------------------------------- 1 | from changes.testutils import APITestCase 2 | 3 | 4 | class NodeIndexTest(APITestCase): 5 | def test_simple(self): 6 | node_1 = self.create_node(label='bar') 7 | node_2 = self.create_node(label='foo') 8 | path = '/api/0/nodes/' 9 | 10 | resp = self.client.get(path) 11 | assert resp.status_code == 200 12 | data = self.unserialize(resp) 13 | assert len(data) == 2 14 | assert data[0]['id'] == node_1.id.hex 15 | assert data[1]['id'] == node_2.id.hex 16 | -------------------------------------------------------------------------------- /migrations/versions/26f665189ca0_remove_jobstep_jobph.py: -------------------------------------------------------------------------------- 1 | """Remove JobStep/JobPhase repository_id 2 | 3 | Revision ID: 26f665189ca0 4 | Revises: 524b3c27203b 5 | Create Date: 2014-01-05 22:01:02.648719 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '26f665189ca0' 11 | down_revision = '524b3c27203b' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.drop_column('jobstep', 'repository_id') 18 | op.drop_column('jobphase', 'repository_id') 19 | 20 | 21 | def downgrade(): 22 | raise NotImplementedError 23 | -------------------------------------------------------------------------------- /migrations/versions/2b4219a6cf46_jobphase_build_id_jo.py: -------------------------------------------------------------------------------- 1 | """JobPhase.build_id => job_id 2 | 3 | Revision ID: 2b4219a6cf46 4 | Revises: f26b6cb3c9c 5 | Create Date: 2013-12-26 00:16:20.974336 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2b4219a6cf46' 11 | down_revision = 'f26b6cb3c9c' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute('ALTER TABLE jobphase RENAME COLUMN build_id TO job_id') 18 | 19 | 20 | def downgrade(): 21 | op.execute('ALTER TABLE jobphase RENAME COLUMN job_id TO build_id') 22 | -------------------------------------------------------------------------------- /migrations/versions/35af40cebcde_jobplan_build_id_job.py: -------------------------------------------------------------------------------- 1 | """JobPlan.build_id => job_id 2 | 3 | Revision ID: 35af40cebcde 4 | Revises: 2b4219a6cf46 5 | Create Date: 2013-12-26 00:18:58.945167 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '35af40cebcde' 11 | down_revision = '2b4219a6cf46' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute('ALTER TABLE jobplan RENAME COLUMN build_id TO job_id') 18 | 19 | 20 | def downgrade(): 21 | op.execute('ALTER TABLE jobplan RENAME COLUMN job_id TO build_id') 22 | -------------------------------------------------------------------------------- /migrations/versions/f26b6cb3c9c_jobstep_build_id_job.py: -------------------------------------------------------------------------------- 1 | """JobStep.build_id => job_id 2 | 3 | Revision ID: f26b6cb3c9c 4 | Revises: 5026dbcee21d 5 | Create Date: 2013-12-26 00:11:38.414155 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = 'f26b6cb3c9c' 11 | down_revision = '5026dbcee21d' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute('ALTER TABLE jobstep RENAME COLUMN build_id TO job_id') 18 | 19 | 20 | def downgrade(): 21 | op.execute('ALTER TABLE jobstep RENAME COLUMN job_id TO build_id') 22 | -------------------------------------------------------------------------------- /migrations/versions/10403d977f5f_add_plan_status.py: -------------------------------------------------------------------------------- 1 | """Add Plan.status 2 | 3 | Revision ID: 10403d977f5f 4 | Revises: 57e4a0553903 5 | Create Date: 2014-08-27 11:56:07.902348 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '10403d977f5f' 11 | down_revision = '57e4a0553903' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('plan', sa.Column('status', sa.Enum(), server_default='1', nullable=False)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('plan', 'status') 23 | -------------------------------------------------------------------------------- /migrations/versions/17d61d9621cb_add_command_type.py: -------------------------------------------------------------------------------- 1 | """Add Command.type 2 | 3 | Revision ID: 17d61d9621cb 4 | Revises: 46a612aff7d3 5 | Create Date: 2014-08-18 13:25:39.609575 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '17d61d9621cb' 11 | down_revision = '46a612aff7d3' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('command', sa.Column('type', sa.Enum(), server_default='0', nullable=False)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('command', 'type') 23 | -------------------------------------------------------------------------------- /migrations/versions/4114cbbd0573_index_job_status_dat.py: -------------------------------------------------------------------------------- 1 | """Index Job.{status,date_created} 2 | 3 | Revision ID: 4114cbbd0573 4 | Revises: 5508859bed73 5 | Create Date: 2014-01-06 11:28:15.691391 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4114cbbd0573' 11 | down_revision = '5508859bed73' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_index('idx_job_status_date_created', 'job', ['status', 'date_created']) 18 | 19 | 20 | def downgrade(): 21 | op.drop_index('idx_job_status_date_created', 'job') 22 | -------------------------------------------------------------------------------- /migrations/versions/4ffb7e1df217_unique_jobphase_job_.py: -------------------------------------------------------------------------------- 1 | """Unique JobPhase.{job_id,label} 2 | 3 | Revision ID: 4ffb7e1df217 4 | Revises: 545e104c5f5a 5 | Create Date: 2014-01-21 11:12:10.408310 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4ffb7e1df217' 11 | down_revision = '545e104c5f5a' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_unique_constraint('unq_jobphase_key', 'jobphase', ['job_id', 'label']) 18 | 19 | 20 | def downgrade(): 21 | op.drop_constraint('unq_jobphase_key', 'jobphase') 22 | -------------------------------------------------------------------------------- /migrations/versions/51775a13339d_patch_hash_column.py: -------------------------------------------------------------------------------- 1 | """patch hash column 2 | 3 | Revision ID: 51775a13339d 4 | Revises: 187eade64ef0 5 | Create Date: 2016-06-17 13:46:10.921685 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '51775a13339d' 11 | down_revision = '187eade64ef0' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('revision', sa.Column('patch_hash', sa.String(40), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('revision', 'patch_hash') 23 | -------------------------------------------------------------------------------- /migrations/versions/545e104c5f5a_add_logsource_step_i.py: -------------------------------------------------------------------------------- 1 | """Add LogSource.step_id 2 | 3 | Revision ID: 545e104c5f5a 4 | Revises: 5677ef75c712 5 | Create Date: 2014-01-15 17:18:53.402012 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '545e104c5f5a' 11 | down_revision = '5677ef75c712' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('logsource', sa.Column('step_id', sa.GUID(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('logsource', 'step_id') 23 | -------------------------------------------------------------------------------- /migrations/versions/3a3366fb7822_index_testgroup_test.py: -------------------------------------------------------------------------------- 1 | """Index testgroup_test.test_id 2 | 3 | Revision ID: 3a3366fb7822 4 | Revises: 139e272152de 5 | Create Date: 2014-01-02 22:20:55.132222 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3a3366fb7822' 11 | down_revision = '139e272152de' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_index('idx_testgroup_test_test_id', 'testgroup_test', ['test_id']) 18 | 19 | 20 | def downgrade(): 21 | op.drop_index('idx_testgroup_test_test_id', 'testgroup_test') 22 | -------------------------------------------------------------------------------- /migrations/versions/3c93047c66c8_add_index_to_jobstep_status.py: -------------------------------------------------------------------------------- 1 | """Add index to JobStep.status 2 | 3 | Revision ID: 3c93047c66c8 4 | Revises: 4ab789ea6b6f 5 | Create Date: 2015-04-28 22:55:23.139295 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3c93047c66c8' 11 | down_revision = '4ab789ea6b6f' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_index('idx_jobstep_status', 'jobstep', ['status'], unique=False) 18 | 19 | 20 | def downgrade(): 21 | op.drop_index('idx_jobstep_status', table_name='jobstep') 22 | -------------------------------------------------------------------------------- /migrations/versions/46a612aff7d3_add_snapshot_source_id.py: -------------------------------------------------------------------------------- 1 | """Add Snapshot.source_id 2 | 3 | Revision ID: 46a612aff7d3 4 | Revises: 2da2dd72d21c 5 | Create Date: 2014-08-12 15:52:28.488096 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '46a612aff7d3' 11 | down_revision = '2da2dd72d21c' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('snapshot', sa.Column('source_id', sa.GUID(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('snapshot', 'source_id') 23 | -------------------------------------------------------------------------------- /migrations/versions/19168fe64c41_unique_constraint_on_command.py: -------------------------------------------------------------------------------- 1 | """Unique constraint on Command 2 | 3 | Revision ID: 19168fe64c41 4 | Revises: 4768ba7627ac 5 | Create Date: 2014-07-17 14:42:59.309732 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '19168fe64c41' 11 | down_revision = '4768ba7627ac' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_unique_constraint('unq_command_order', 'command', ['jobstep_id', 'order']) 18 | 19 | 20 | def downgrade(): 21 | op.drop_constraint('unq_command_order', 'command') 22 | -------------------------------------------------------------------------------- /migrations/versions/256ab638deaa_filecoverage_build_i.py: -------------------------------------------------------------------------------- 1 | """FileCoverage.build_id => job_id 2 | 3 | Revision ID: 256ab638deaa 4 | Revises: 37244bf4e3f5 5 | Create Date: 2013-12-26 01:47:50.855439 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '256ab638deaa' 11 | down_revision = '37244bf4e3f5' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute('ALTER TABLE filecoverage RENAME COLUMN build_id TO job_id') 18 | 19 | 20 | def downgrade(): 21 | op.execute('ALTER TABLE filecoverage RENAME COLUMN job_id TO build_id') 22 | -------------------------------------------------------------------------------- /migrations/versions/2596f21c6f58_add_project_status.py: -------------------------------------------------------------------------------- 1 | """Add Project.status 2 | 3 | Revision ID: 2596f21c6f58 4 | Revises: 4e68c2a3d269 5 | Create Date: 2014-02-18 17:34:59.432346 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2596f21c6f58' 11 | down_revision = '4e68c2a3d269' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('project', sa.Column('status', sa.Enum(), server_default='1', nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('project', 'status') 23 | -------------------------------------------------------------------------------- /migrations/versions/36cbde703cc0_add_build_priority.py: -------------------------------------------------------------------------------- 1 | """Add Build.priority 2 | 3 | Revision ID: 36cbde703cc0 4 | Revises: fe743605e1a 5 | Create Date: 2014-10-06 10:10:14.729720 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '36cbde703cc0' 11 | down_revision = '2c6662281b66' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('build', sa.Column('priority', sa.Enum(), server_default='0', nullable=False)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('build', 'priority') 23 | -------------------------------------------------------------------------------- /migrations/versions/41c7ef24fd4c_add_build_date_decided.py: -------------------------------------------------------------------------------- 1 | """add Build.date_decided 2 | 3 | Revision ID: 41c7ef24fd4c 4 | Revises: 3961ccb5d884 5 | Create Date: 2015-12-16 13:27:24.838841 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '41c7ef24fd4c' 11 | down_revision = '3961ccb5d884' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('build', sa.Column('date_decided', sa.DateTime(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('build', 'date_decided') 23 | -------------------------------------------------------------------------------- /migrations/versions/4c3f2a63b7a7_add_plan_avg_build_time.py: -------------------------------------------------------------------------------- 1 | """Add Plan.avg_build_time 2 | 3 | Revision ID: 4c3f2a63b7a7 4 | Revises: 4cd8cf6a0894 5 | Create Date: 2014-10-09 15:21:09.463279 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4c3f2a63b7a7' 11 | down_revision = '4cd8cf6a0894' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('plan', sa.Column('avg_build_time', sa.Integer(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('plan', 'avg_build_time') 23 | -------------------------------------------------------------------------------- /changes/api/serializer/models/author.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.api.serializer.models.user import get_gravatar_url 3 | from changes.models.author import Author 4 | 5 | 6 | @register(Author) 7 | class AuthorCrumbler(Crumbler): 8 | def crumble(self, instance, attrs): 9 | return { 10 | 'id': instance.id.hex, 11 | 'name': instance.name, 12 | 'email': instance.email, 13 | 'avatar': get_gravatar_url(instance.email), 14 | 'dateCreated': instance.date_created, 15 | } 16 | -------------------------------------------------------------------------------- /changes/api/serializer/models/event.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.event import Event 3 | 4 | 5 | @register(Event) 6 | class EventCrumbler(Crumbler): 7 | def crumble(self, instance, attrs): 8 | return { 9 | 'id': instance.id.hex, 10 | 'type': instance.type, 11 | 'itemId': instance.item_id.hex, 12 | 'data': dict(instance.data), 13 | 'dateCreated': instance.date_created.isoformat(), 14 | 'dateModified': instance.date_modified.isoformat(), 15 | } 16 | -------------------------------------------------------------------------------- /migrations/versions/3d8177efcfe1_add_filecoverage_step_id.py: -------------------------------------------------------------------------------- 1 | """Add FileCoverage.step_id 2 | 3 | Revision ID: 3d8177efcfe1 4 | Revises: 3df65ebfa27e 5 | Create Date: 2014-05-08 11:19:04.251803 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3d8177efcfe1' 11 | down_revision = '3df65ebfa27e' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('filecoverage', sa.Column('step_id', sa.GUID(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('filecoverage', 'step_id') 23 | -------------------------------------------------------------------------------- /tests/changes/api/test_cluster_index.py: -------------------------------------------------------------------------------- 1 | from changes.testutils import APITestCase 2 | 3 | 4 | class ClusterIndexTest(APITestCase): 5 | def test_simple(self): 6 | cluster_1 = self.create_cluster(label='bar') 7 | cluster_2 = self.create_cluster(label='foo') 8 | path = '/api/0/clusters/' 9 | 10 | resp = self.client.get(path) 11 | assert resp.status_code == 200 12 | data = self.unserialize(resp) 13 | assert len(data) == 2 14 | assert data[0]['id'] == cluster_1.id.hex 15 | assert data[1]['id'] == cluster_2.id.hex 16 | -------------------------------------------------------------------------------- /migrations/versions/2b7153fe25af_add_repository_status.py: -------------------------------------------------------------------------------- 1 | """Add Repository.status 2 | 3 | Revision ID: 2b7153fe25af 4 | Revises: 3b22c1663664 5 | Create Date: 2014-07-03 12:18:06.616440 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2b7153fe25af' 11 | down_revision = '3b22c1663664' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('repository', sa.Column('status', sa.Enum(), server_default='1', nullable=False)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('repository', 'status') 23 | -------------------------------------------------------------------------------- /migrations/versions/2d9df8a3103c_add_logchunk_unique_.py: -------------------------------------------------------------------------------- 1 | """Add LogChunk unique constraint 2 | 3 | Revision ID: 2d9df8a3103c 4 | Revises: 393be9b08e4c 5 | Create Date: 2013-11-13 17:12:48.662009 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2d9df8a3103c' 11 | down_revision = '393be9b08e4c' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_unique_constraint('unq_logchunk_source_offset', 'logchunk', ['source_id', 'offset']) 18 | 19 | 20 | def downgrade(): 21 | op.drop_constraint('unq_logchunk_source_offset', 'logchunk') 22 | -------------------------------------------------------------------------------- /migrations/versions/2da2dd72d21c_add_snapshot_date_created.py: -------------------------------------------------------------------------------- 1 | """Add Snapshot.date_created 2 | 3 | Revision ID: 2da2dd72d21c 4 | Revises: 2191c871434 5 | Create Date: 2014-08-08 14:29:38.740485 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2da2dd72d21c' 11 | down_revision = '2191c871434' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('snapshot', sa.Column('date_created', sa.DateTime(), nullable=False)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('snapshot', 'date_created') 23 | -------------------------------------------------------------------------------- /migrations/versions/3961ccb5d884_increase_artifact_name_length.py: -------------------------------------------------------------------------------- 1 | """increase artifact name length 2 | 3 | Revision ID: 3961ccb5d884 4 | Revises: 1b229c83511d 5 | Create Date: 2015-11-05 15:34:28.189700 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3961ccb5d884' 11 | down_revision = '1b229c83511d' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.alter_column('artifact', 'name', type_=sa.VARCHAR(1024)) 19 | 20 | 21 | def downgrade(): 22 | op.alter_column('artifact', 'name', type_=sa.VARCHAR(128)) 23 | -------------------------------------------------------------------------------- /webapp/css/wiki_external_link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /migrations/versions/016f138b2da8_add_job_autogenerated.py: -------------------------------------------------------------------------------- 1 | """add job autogenerated 2 | 3 | Revision ID: 016f138b2da8 4 | Revises: 58c2302ec362 5 | Create Date: 2016-06-09 17:32:41.079003 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '016f138b2da8' 11 | down_revision = '58c2302ec362' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('job', sa.Column('autogenerated', sa.Boolean(), nullable=False, server_default="0")) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('job', 'autogenerated') 23 | -------------------------------------------------------------------------------- /migrations/versions/15bd4b7e6622_add_filecoverage_unique_constraint.py: -------------------------------------------------------------------------------- 1 | """Add FileCoverage unique constraint 2 | 3 | Revision ID: 15bd4b7e6622 4 | Revises: 3d8177efcfe1 5 | Create Date: 2014-05-09 11:06:50.845168 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '15bd4b7e6622' 11 | down_revision = '3d8177efcfe1' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_unique_constraint('unq_job_filname', 'filecoverage', ['job_id', 'filename']) 18 | 19 | 20 | def downgrade(): 21 | op.drop_constraint('unq_job_filname', 'filecoverage') 22 | -------------------------------------------------------------------------------- /migrations/versions/208224023555_add_repository_backe.py: -------------------------------------------------------------------------------- 1 | """Add Repository.backend 2 | 3 | Revision ID: 208224023555 4 | Revises: 1d1f467bdf3d 5 | Create Date: 2013-11-25 15:12:30.867388 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '208224023555' 11 | down_revision = '1d1f467bdf3d' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('repository', sa.Column('backend', sa.Enum(), nullable=False, server_default='0')) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('repository', 'backend') 23 | -------------------------------------------------------------------------------- /migrations/versions/2fa8391277a0_add_jobstep_last_heartbeat.py: -------------------------------------------------------------------------------- 1 | """Add JobStep.last_heartbeat 2 | 3 | Revision ID: 2fa8391277a0 4 | Revises: 3768db8af6ea 5 | Create Date: 2014-11-04 16:14:32.624975 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2fa8391277a0' 11 | down_revision = '3768db8af6ea' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('jobstep', sa.Column('last_heartbeat', sa.DateTime(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('jobstep', 'last_heartbeat') 23 | -------------------------------------------------------------------------------- /migrations/versions/3f289637f530_remove_unused_models.py: -------------------------------------------------------------------------------- 1 | """Remove unused models 2 | 3 | Revision ID: 3f289637f530 4 | Revises: 4ba1dd8c3080 5 | Create Date: 2014-04-17 11:08:50.963964 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3f289637f530' 11 | down_revision = '4ba1dd8c3080' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.drop_table('aggtestgroup') 18 | op.drop_table('testgroup_test') 19 | op.drop_table('testgroup') 20 | op.drop_table('aggtestsuite') 21 | 22 | 23 | def downgrade(): 24 | raise NotImplementedError 25 | -------------------------------------------------------------------------------- /migrations/versions/520eba1ce36e_remove_author_name_u.py: -------------------------------------------------------------------------------- 1 | """Remove Author.name unique constraint 2 | 3 | Revision ID: 520eba1ce36e 4 | Revises: 18c045b75331 5 | Create Date: 2013-11-04 19:20:04.277883 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '520eba1ce36e' 11 | down_revision = '18c045b75331' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.drop_constraint('author_name_key', 'author') 18 | 19 | 20 | def downgrade(): 21 | ### commands auto generated by Alembic - please adjust! ### 22 | pass 23 | ### end Alembic commands ### 24 | -------------------------------------------------------------------------------- /migrations/versions/554f414d4c46_set_on_delete_cascad.py: -------------------------------------------------------------------------------- 1 | """Set ON DELETE CASCADE on Step.* 2 | 3 | Revision ID: 554f414d4c46 4 | Revises: 306fefe51dc6 5 | Create Date: 2013-12-23 16:46:05.137414 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '554f414d4c46' 11 | down_revision = '306fefe51dc6' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.drop_constraint('step_plan_id_fkey', 'step') 18 | op.create_foreign_key('step_plan_id_fkey', 'step', 'plan', ['plan_id'], ['id'], ondelete='CASCADE') 19 | 20 | 21 | def downgrade(): 22 | pass 23 | -------------------------------------------------------------------------------- /migrations/versions/f8ed1f99eb1_add_projectplan_avg_.py: -------------------------------------------------------------------------------- 1 | """Add ProjectPlan.avg_build_time 2 | 3 | Revision ID: f8ed1f99eb1 4 | Revises: 25d63e09ea3b 5 | Create Date: 2014-01-02 16:12:43.339060 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = 'f8ed1f99eb1' 11 | down_revision = '25d63e09ea3b' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('project_plan', sa.Column('avg_build_time', sa.Integer(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('project_plan', 'avg_build_time') 23 | -------------------------------------------------------------------------------- /changes/api/build_message_index.py: -------------------------------------------------------------------------------- 1 | from changes.api.base import APIView, error 2 | from changes.models.build import Build 3 | from changes.models.buildmessage import BuildMessage 4 | 5 | 6 | class BuildMessageIndexAPIView(APIView): 7 | def get(self, build_id): 8 | build = Build.query.get(build_id) 9 | if not build: 10 | return error('build not found', http_code=404) 11 | queryset = BuildMessage.query.filter( 12 | BuildMessage.build_id == build.id, 13 | ).order_by(BuildMessage.date_created.asc()) 14 | 15 | return self.paginate(queryset) 16 | -------------------------------------------------------------------------------- /changes/api/node_from_hostname.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from changes.api.base import APIView, error 4 | from changes.models.node import Node 5 | 6 | 7 | class NodeFromHostnameAPIView(APIView): 8 | def get(self, node_hostname): 9 | node = Node.query.filter(Node.label == node_hostname).first() 10 | if node is None: 11 | return error("Node not found", http_code=404) 12 | 13 | context = self.serialize(node) 14 | context['clusters'] = self.serialize(list(node.clusters)) 15 | 16 | return self.respond(context, serialize=False) 17 | -------------------------------------------------------------------------------- /migrations/versions/57e4a0553903_add_snapshotimage_status.py: -------------------------------------------------------------------------------- 1 | """Add SnapshotImage.status 2 | 3 | Revision ID: 57e4a0553903 4 | Revises: 30c86cc72856 5 | Create Date: 2014-08-21 23:03:23.553069 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '57e4a0553903' 11 | down_revision = '30c86cc72856' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('snapshot_image', sa.Column('status', sa.Enum(), server_default='0', nullable=False)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('snapshot_image', 'status') 23 | -------------------------------------------------------------------------------- /migrations/versions/8550c7394f4_change_commands_env_to_text.py: -------------------------------------------------------------------------------- 1 | """change commands.env to text 2 | 3 | Revision ID: 8550c7394f4 4 | Revises: 5844fbc9d9e4 5 | Create Date: 2015-06-15 13:42:13.990095 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '8550c7394f4' 11 | down_revision = '5844fbc9d9e4' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.alter_column('command', 'env', type_ = sa.types.Text()) 19 | 20 | 21 | def downgrade(): 22 | op.alter_column('command', 'env', type_ = sa.types.String(length=2048)) 23 | 24 | -------------------------------------------------------------------------------- /migrations/versions/18c150792a21_add_column_flakyteststat_first_run.py: -------------------------------------------------------------------------------- 1 | """Add column flakyteststat.first_run 2 | 3 | Revision ID: 18c150792a21 4 | Revises: 3bc4c6853367 5 | Create Date: 2015-06-03 18:29:05.131373 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '18c150792a21' 11 | down_revision = '3bc4c6853367' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('flakyteststat', sa.Column('first_run', sa.Date(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('flakyteststat', 'first_run') 23 | -------------------------------------------------------------------------------- /migrations/versions/30b9f619c3b2_remove_test_suite.py: -------------------------------------------------------------------------------- 1 | """Remove test suite 2 | 3 | Revision ID: 30b9f619c3b2 4 | Revises: 3b55ff8856f5 5 | Create Date: 2014-05-09 14:36:41.548924 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '30b9f619c3b2' 11 | down_revision = '3b55ff8856f5' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_unique_constraint('unq_test_name', 'test', ['job_id', 'label_sha']) 18 | op.drop_column('test', 'suite_id') 19 | 20 | op.drop_table('testsuite') 21 | 22 | 23 | def downgrade(): 24 | raise NotImplementedError 25 | -------------------------------------------------------------------------------- /migrations/versions/17c884f6f57c_add_failurereason_column.py: -------------------------------------------------------------------------------- 1 | """add failurereason column 2 | 3 | Revision ID: 17c884f6f57c 4 | Revises: 3cf79ad9bde 5 | Create Date: 2015-07-29 08:22:59.755597 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '17c884f6f57c' 11 | down_revision = '3cf79ad9bde' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column( 19 | 'failurereason', 20 | sa.Column('data', sa.JSONEncodedDict(), nullable=True)) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column('failurereason', 'data') 25 | -------------------------------------------------------------------------------- /migrations/versions/1b229c83511d_add_logsource_in_artifact_store.py: -------------------------------------------------------------------------------- 1 | """add_logsource_in_artifact_store 2 | 3 | Revision ID: 1b229c83511d 4 | Revises: 382745ed4478 5 | Create Date: 2015-10-07 14:55:58.423046 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '1b229c83511d' 11 | down_revision = '382745ed4478' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('logsource', sa.Column('in_artifact_store', sa.Boolean(), default=False)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('logsource', 'in_artifact_store') 23 | -------------------------------------------------------------------------------- /migrations/versions/d324e8fc580_add_various_testsuit.py: -------------------------------------------------------------------------------- 1 | """Add various TestSuite indexes 2 | 3 | Revision ID: d324e8fc580 4 | Revises: 3bef10ab5088 5 | Create Date: 2013-11-04 17:12:09.353301 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = 'd324e8fc580' 11 | down_revision = '3bef10ab5088' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_index('idx_testsuite_project_id', 'testsuite', ['project_id']) 18 | 19 | 20 | def downgrade(): 21 | ### commands auto generated by Alembic - please adjust! ### 22 | pass 23 | ### end Alembic commands ### 24 | -------------------------------------------------------------------------------- /changes/jobs/check_repos.py: -------------------------------------------------------------------------------- 1 | from changes.jobs.sync_repo import sync_repo 2 | from changes.models.repository import Repository, RepositoryBackend 3 | 4 | 5 | def check_repos(): 6 | """ 7 | Looks for any repositories which haven't checked in within several minutes 8 | and creates `sync_repo` tasks for them. 9 | """ 10 | repo_list = list(Repository.query.filter( 11 | Repository.backend != RepositoryBackend.unknown, 12 | )) 13 | 14 | for repo in repo_list: 15 | sync_repo.delay_if_needed( 16 | task_id=repo.id.hex, 17 | repo_id=repo.id.hex, 18 | ) 19 | -------------------------------------------------------------------------------- /migrations/versions/291978a5d5e4_add_revision_date_created_signal.py: -------------------------------------------------------------------------------- 1 | """add revision date created signal 2 | 3 | Revision ID: 291978a5d5e4 4 | Revises: 17c884f6f57c 5 | Create Date: 2015-07-31 14:17:01.337179 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '291978a5d5e4' 11 | down_revision = '17c884f6f57c' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('revision', sa.Column('date_created_signal', sa.DateTime(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('revision', 'date_created_signal') 23 | -------------------------------------------------------------------------------- /migrations/versions/3aed22af8f4f_add_build_tag.py: -------------------------------------------------------------------------------- 1 | """Add Build.tags 2 | 3 | Revision ID: 3aed22af8f4f 4 | Revises: 4e2d942b58b0 5 | Create Date: 2014-12-01 12:38:25.867513 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3aed22af8f4f' 11 | down_revision = '4e2d942b58b0' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | from sqlalchemy.dialects import postgresql 16 | 17 | 18 | def upgrade(): 19 | op.add_column('build', sa.Column('tags', postgresql.ARRAY(sa.String(length=16)), nullable=True)) 20 | 21 | 22 | def downgrade(): 23 | op.drop_column('build', 'tags') 24 | -------------------------------------------------------------------------------- /migrations/versions/1db7a1ab95db_fix_filecoverage_pro.py: -------------------------------------------------------------------------------- 1 | """Fix FileCoverage.project_id 2 | 3 | Revision ID: 1db7a1ab95db 4 | Revises: 36becb086fcb 5 | Create Date: 2014-03-21 15:47:41.430619 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '1db7a1ab95db' 11 | down_revision = '36becb086fcb' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.drop_column('filecoverage', 'project_id') 19 | op.add_column('filecoverage', sa.Column('project_id', sa.GUID(), nullable=False)) 20 | 21 | 22 | def downgrade(): 23 | raise NotImplementedError 24 | -------------------------------------------------------------------------------- /migrations/versions/3b22c1663664_change_logsource_constraint.py: -------------------------------------------------------------------------------- 1 | """Change LogSource constraint 2 | 3 | Revision ID: 3b22c1663664 4 | Revises: 15f131d88adf 5 | Create Date: 2014-06-25 01:07:42.217354 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3b22c1663664' 11 | down_revision = '15f131d88adf' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_unique_constraint( 18 | 'unq_logsource_key2', 'logsource', ['step_id', 'name']) 19 | op.drop_constraint('unq_logsource_key', 'logsource') 20 | 21 | 22 | def downgrade(): 23 | raise NotImplementedError 24 | -------------------------------------------------------------------------------- /webapp/display/section_header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* 4 | * Very simple text component designed to be the title above grid elements 5 | */ 6 | var SectionHeader = React.createClass({ 7 | 8 | propTypes: { 9 | // ... 10 | // transfers all properties to rendered
11 | }, 12 | 13 | render: function() { 14 | var { className, ...others } = this.props; 15 | className = (className || "") + " sectionHeader nonFixedClass"; 16 | return
17 | {this.props.children} 18 |
; 19 | } 20 | }); 21 | 22 | export default SectionHeader; 23 | -------------------------------------------------------------------------------- /changes/api/repository_project_index.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, unicode_literals 2 | 3 | from changes.api.base import APIView 4 | from changes.models.project import Project 5 | from changes.models.repository import Repository 6 | 7 | 8 | class RepositoryProjectIndexAPIView(APIView): 9 | def get(self, repository_id): 10 | repo = Repository.query.get(repository_id) 11 | if repo is None: 12 | return '', 404 13 | 14 | queryset = Project.query.filter( 15 | Project.repository_id == repo.id, 16 | ) 17 | 18 | return self.paginate(queryset) 19 | -------------------------------------------------------------------------------- /migrations/versions/3042d0ca43bf_index_job_project_id.py: -------------------------------------------------------------------------------- 1 | """Index Job(project_id, status, date_created) where patch_id IS NULL 2 | 3 | Revision ID: 3042d0ca43bf 4 | Revises: 3a3366fb7822 5 | Create Date: 2014-01-03 15:24:39.947813 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3042d0ca43bf' 11 | down_revision = '3a3366fb7822' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute('CREATE INDEX idx_job_previous_runs ON job (project_id, status, date_created) WHERE patch_id IS NULL') 18 | 19 | 20 | def downgrade(): 21 | op.drop_index('idx_job_previous_runs', 'job') 22 | -------------------------------------------------------------------------------- /migrations/versions/4a12e7f0159d_remove_build_repository_id_revision_sha_.py: -------------------------------------------------------------------------------- 1 | """Remove Build.{repository_id,revision_sha,patch_id} 2 | 3 | Revision ID: 4a12e7f0159d 4 | Revises: 1b2fa9c97090 5 | Create Date: 2014-05-15 12:12:19.106724 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4a12e7f0159d' 11 | down_revision = '1b2fa9c97090' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.drop_column('build', 'revision_sha') 18 | op.drop_column('build', 'repository_id') 19 | op.drop_column('build', 'patch_id') 20 | 21 | 22 | def downgrade(): 23 | raise NotImplementedError 24 | -------------------------------------------------------------------------------- /migrations/versions/5844fbc9d9e4_add_flakyteststat_double_reruns.py: -------------------------------------------------------------------------------- 1 | """Add flakyteststat.double_reruns 2 | 3 | Revision ID: 5844fbc9d9e4 4 | Revises: 18c150792a21 5 | Create Date: 2015-06-04 14:20:02.697048 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '5844fbc9d9e4' 11 | down_revision = '18c150792a21' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('flakyteststat', sa.Column('double_reruns', sa.Integer(), nullable=False, server_default='0')) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('flakyteststat', 'double_reruns') 23 | -------------------------------------------------------------------------------- /tests/changes/api/test_testcase_details.py: -------------------------------------------------------------------------------- 1 | from changes.testutils import APITestCase 2 | 3 | 4 | class TestCaseDetailsTest(APITestCase): 5 | def test_simple(self): 6 | project = self.create_project() 7 | build = self.create_build(project=project) 8 | job = self.create_job(build=build) 9 | 10 | testcase = self.create_test(job=job) 11 | 12 | path = '/api/0/tests/{0}/'.format( 13 | testcase.id.hex) 14 | 15 | resp = self.client.get(path) 16 | assert resp.status_code == 200 17 | data = self.unserialize(resp) 18 | assert data['id'] == testcase.id.hex 19 | -------------------------------------------------------------------------------- /changes/db/types/enum.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.types import TypeDecorator, INT 2 | 3 | 4 | class Enum(TypeDecorator): 5 | impl = INT 6 | 7 | def __init__(self, enum=None, *args, **kwargs): 8 | self.enum = enum 9 | super(Enum, self).__init__(*args, **kwargs) 10 | 11 | def process_bind_param(self, value, dialect): 12 | if value is None: 13 | return value 14 | return value.value 15 | 16 | def process_result_value(self, value, dialect): 17 | if value is None: 18 | return value 19 | elif self.enum: 20 | return self.enum(value) 21 | return value 22 | -------------------------------------------------------------------------------- /changes/api/cluster_details.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from changes.api.base import APIView 4 | from changes.models.node import Cluster, Node 5 | 6 | 7 | class ClusterDetailsAPIView(APIView): 8 | def get(self, cluster_id): 9 | cluster = Cluster.query.get(cluster_id) 10 | if cluster is None: 11 | return '', 404 12 | 13 | node_count = Node.query.filter( 14 | Node.clusters.contains(cluster), 15 | ).count() 16 | 17 | context = self.serialize(cluster) 18 | context['numNodes'] = node_count 19 | 20 | return self.respond(context, serialize=False) 21 | -------------------------------------------------------------------------------- /migrations/versions/36becb086fcb_add_revision_branche.py: -------------------------------------------------------------------------------- 1 | """Add Revision.branches 2 | 3 | Revision ID: 36becb086fcb 4 | Revises: 11a7a7f2652f 5 | Create Date: 2014-03-18 17:48:37.484301 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '36becb086fcb' 11 | down_revision = '11a7a7f2652f' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | from sqlalchemy.dialects import postgresql 16 | 17 | 18 | def upgrade(): 19 | op.add_column('revision', sa.Column('branches', postgresql.ARRAY(sa.String(length=128)), nullable=True)) 20 | 21 | 22 | def downgrade(): 23 | op.drop_column('revision', 'branches') 24 | -------------------------------------------------------------------------------- /tests/changes/utils/test_trees.py: -------------------------------------------------------------------------------- 1 | from changes.utils.trees import build_tree 2 | 3 | 4 | def test_build_tree(): 5 | test_names = [ 6 | 'foo.bar.bar', 7 | 'foo.bar.biz', 8 | 'foo.biz', 9 | 'blah.brah', 10 | 'blah.blah.blah', 11 | ] 12 | 13 | result = build_tree(test_names, min_children=2) 14 | 15 | assert sorted(result) == ['blah', 'foo'] 16 | 17 | result = build_tree(test_names, min_children=2, parent='foo') 18 | 19 | assert sorted(result) == ['foo.bar', 'foo.biz'] 20 | 21 | result = build_tree(test_names, min_children=2, parent='foo.biz') 22 | 23 | assert result == set() 24 | -------------------------------------------------------------------------------- /migrations/versions/3d9067f21201_unique_step_order.py: -------------------------------------------------------------------------------- 1 | """Unique Step.order 2 | 3 | Revision ID: 3d9067f21201 4 | Revises: 2622a69cd25a 5 | Create Date: 2013-12-18 15:06:47.035804 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3d9067f21201' 11 | down_revision = '2622a69cd25a' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_unique_constraint('unq_step_key', 'step', ['plan_id', 'order']) 18 | op.drop_index('idx_step_plan_id', 'step') 19 | 20 | 21 | def downgrade(): 22 | op.create_index('idx_step_plan_id', 'step', ['plan_id']) 23 | op.drop_constraint('unq_step_key', 'step') 24 | -------------------------------------------------------------------------------- /tests/changes/api/test_project_plan_index.py: -------------------------------------------------------------------------------- 1 | from changes.testutils import APITestCase 2 | 3 | 4 | class ProjectPlanListTest(APITestCase): 5 | def test_retrieve(self): 6 | project = self.create_project() 7 | project2 = self.create_project() 8 | path = '/api/0/projects/{0}/plans/'.format( 9 | project.id.hex) 10 | 11 | plan = self.create_plan(project) 12 | self.create_plan(project2) 13 | 14 | resp = self.client.get(path) 15 | assert resp.status_code == 200 16 | data = self.unserialize(resp) 17 | assert len(data) == 1 18 | assert data[0]['id'] == plan.id.hex 19 | -------------------------------------------------------------------------------- /migrations/versions/2d82db02b3ef_fill_testgroup_num_l.py: -------------------------------------------------------------------------------- 1 | """Fill TestGroup.num_leaves 2 | 3 | Revision ID: 2d82db02b3ef 4 | Revises: 4640ecd97c82 5 | Create Date: 2013-11-08 13:14:05.257558 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2d82db02b3ef' 11 | down_revision = '4640ecd97c82' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute(""" 18 | update testgroup as a 19 | set num_leaves = ( 20 | select count(*) 21 | from testgroup as b 22 | where a.id = b.parent_id 23 | ) 24 | """) 25 | 26 | 27 | def downgrade(): 28 | pass 29 | -------------------------------------------------------------------------------- /migrations/versions/3b94584c761e_create_index_in_flakyteststat_for_test_.py: -------------------------------------------------------------------------------- 1 | """Create index in flakyteststat for test_id 2 | 3 | Revision ID: 3b94584c761e 4 | Revises: 3be107806e62 5 | Create Date: 2016-07-13 14:03:31.967630 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3b94584c761e' 11 | down_revision = '3be107806e62' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.create_index('idx_flakyteststat_last_flaky_run_id', 'flakyteststat', ['last_flaky_run_id']) 19 | 20 | 21 | def downgrade(): 22 | op.drop_index('idx_flakyteststat_last_flaky_run_id', 'flakyteststat') 23 | -------------------------------------------------------------------------------- /migrations/versions/382745ed4478_add_jobplan_snapshotimage.py: -------------------------------------------------------------------------------- 1 | """add jobplan snapshotimage 2 | 3 | Revision ID: 382745ed4478 4 | Revises: 3e85891e90bb 5 | Create Date: 2015-09-25 10:20:05.963135 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '382745ed4478' 11 | down_revision = '3e85891e90bb' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('jobplan', sa.Column('snapshot_image_id', sa.GUID(), 19 | sa.ForeignKey('snapshot_image.id', ondelete="RESTRICT"), nullable=True)) 20 | 21 | 22 | def downgrade(): 23 | op.drop_column('jobplan', 'snapshot_image_id') 24 | -------------------------------------------------------------------------------- /static/vendor/uri.js/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "URIjs", 3 | "version": "1.15.2", 4 | "main": "src/URI.js", 5 | "ignore": [ 6 | ".*", 7 | "*.css", 8 | "/*.js", 9 | "/*.html", 10 | "/*.json", 11 | "utils", 12 | "test", 13 | "prettify" 14 | ], 15 | "license": "MIT", 16 | "homepage": "https://github.com/medialize/URI.js", 17 | "_release": "1.15.2", 18 | "_resolution": { 19 | "type": "version", 20 | "tag": "v1.15.2", 21 | "commit": "c51da7e8c0bdf46ae926d2388d3032e2347c61db" 22 | }, 23 | "_source": "https://github.com/medialize/URI.js.git", 24 | "_target": "~1.15.2", 25 | "_originalSource": "uri.js" 26 | } -------------------------------------------------------------------------------- /changes/api/build_target_message_index.py: -------------------------------------------------------------------------------- 1 | from changes.api.base import APIView, error 2 | from changes.models.bazeltarget import BazelTarget 3 | from changes.models.bazeltargetmessage import BazelTargetMessage 4 | 5 | 6 | class BuildTargetMessageIndex(APIView): 7 | def get(self, build_id, target_id): 8 | target = BazelTarget.query.get(target_id) 9 | if not target: 10 | return error('target not found', http_code=404) 11 | queryset = BazelTargetMessage.query.filter( 12 | BazelTargetMessage.target_id == target.id, 13 | ).order_by(BazelTargetMessage.date_created.asc()) 14 | 15 | return self.paginate(queryset) 16 | -------------------------------------------------------------------------------- /changes/api/serializer/vcs/revisionresult.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.vcs.base import RevisionResult 3 | 4 | 5 | @register(RevisionResult) 6 | class RevisionCrumbler(Crumbler): 7 | def crumble(self, instance, attrs): 8 | return { 9 | 'id': instance.id, 10 | 'sha': instance.id, # Having both id and sha is a bit distasteful. We should try to fix this. 11 | 'message': instance.message, 12 | 'author': None, # We don't return author information 13 | 'dateCreated': instance.author_date, 14 | 'dateCommitted': instance.committer_date, 15 | } 16 | -------------------------------------------------------------------------------- /static/vendor/react-bootstrap/README.md: -------------------------------------------------------------------------------- 1 | # react-bootstrap-bower 2 | 3 | [Bootstrap 3](http://getbootstrap.com) components built with [React](http://facebook.github.io/react/) 4 | 5 | This repo contains built AMD modules and standalone browser globals. 6 | 7 | There is a separate [source repo](https://github.com/react-bootstrap/react-bootstrap). 8 | 9 | A [docs site](http://react-bootstrap.github.io) with live editable examples. 10 | 11 | [![Build Status](https://travis-ci.org/react-bootstrap/react-bootstrap.svg)](https://travis-ci.org/react-bootstrap/react-bootstrap) [![Bower version](https://badge.fury.io/bo/react-bootstrap.svg)](http://badge.fury.io/bo/react-bootstrap) 12 | -------------------------------------------------------------------------------- /migrations/versions/40429d3569cf_add_plan_project_id.py: -------------------------------------------------------------------------------- 1 | """Add Plan.project_id 2 | 3 | Revision ID: 40429d3569cf 4 | Revises: 36cbde703cc0 5 | Create Date: 2014-10-09 14:38:51.043652 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '40429d3569cf' 11 | down_revision = '36cbde703cc0' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('plan', sa.Column('project_id', sa.GUID(), nullable=True)) 19 | op.create_foreign_key('plan_project_id_fkey', 'plan', 'project', ['project_id'], ['id'], ondelete='CASCADE') 20 | 21 | 22 | def downgrade(): 23 | op.drop_column('plan', 'project_id') 24 | -------------------------------------------------------------------------------- /migrations/versions/cb99fdfb903_add_build_family_id.py: -------------------------------------------------------------------------------- 1 | """Add Build.family_id 2 | 3 | Revision ID: cb99fdfb903 4 | Revises: 1109e724859f 5 | Create Date: 2013-12-23 11:32:17.060863 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = 'cb99fdfb903' 11 | down_revision = '1109e724859f' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('build', sa.Column('family_id', sa.GUID(), nullable=True)) 19 | op.create_index('idx_build_family_id', 'build', ['family_id']) 20 | 21 | 22 | def downgrade(): 23 | op.drop_index('idx_build_family_id', 'build') 24 | op.drop_column('build', 'family_id') 25 | -------------------------------------------------------------------------------- /static/vendor/requirejs-babel/README.md: -------------------------------------------------------------------------------- 1 | Babel (6to5) Plugin 2 | === 3 | 4 | A [Babel](https://babeljs.io/) loader plugin for [RequireJS](http://requirejs.org). 5 | 6 | Installation 7 | --- 8 | 9 | ``` 10 | $ npm install -g bower 11 | $ bower install requirejs-babel 12 | ``` 13 | 14 | Usage 15 | --- 16 | 17 | Add the paths to configuration: 18 | 19 | ```javascript 20 | paths: { 21 | es6: '...path_to_bower/requirejs-babel/es6', 22 | babel: '...path_to_bower/requirejs-babel/babel-4.6.6.min' 23 | } 24 | ``` 25 | 26 | Reference files via the es6! plugin name: 27 | ```javascript 28 | define(['es6!your-es6-module'], function(module) { 29 | // ... 30 | }); 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/_themes/kr/layout.html: -------------------------------------------------------------------------------- 1 | {%- extends "basic/layout.html" %} 2 | {%- block extrahead %} 3 | {{ super() }} 4 | {% if theme_touch_icon %} 5 | 6 | {% endif %} 7 | 9 | 10 | {% endblock %} 11 | {%- block relbar2 %}{% endblock %} 12 | {%- block footer %} 13 | 16 | {%- endblock %} 17 | -------------------------------------------------------------------------------- /docs/_themes/kr/relations.html: -------------------------------------------------------------------------------- 1 |

Related Topics

2 | 20 | -------------------------------------------------------------------------------- /migrations/versions/460741ff1212_add_project_permissions_column_to_user.py: -------------------------------------------------------------------------------- 1 | """add project_permissions column to user 2 | 3 | Revision ID: 460741ff1212 4 | Revises: 3b94584c761e 5 | Create Date: 2016-08-19 09:26:10.731482 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '460741ff1212' 11 | down_revision = '3b94584c761e' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | from sqlalchemy.dialects import postgresql 16 | 17 | 18 | def upgrade(): 19 | op.add_column('user', sa.Column('project_permissions', postgresql.ARRAY(sa.String(length=256)), nullable=True)) 20 | 21 | 22 | def downgrade(): 23 | op.drop_column('user', 'project_permissions') 24 | -------------------------------------------------------------------------------- /migrations/versions/1d806848a73f_unique_buildplan_bui.py: -------------------------------------------------------------------------------- 1 | """Unique BuildPlan.build 2 | 3 | Revision ID: 1d806848a73f 4 | Revises: ff220d76c11 5 | Create Date: 2013-12-13 13:37:37.833620 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '1d806848a73f' 11 | down_revision = 'ff220d76c11' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_unique_constraint('unq_buildplan_build', 'buildplan', ['build_id']) 18 | op.drop_index('idx_buildplan_build_id', 'buildplan') 19 | 20 | 21 | def downgrade(): 22 | op.create_index('idx_buildplan_build_id', 'buildplan', ['build_id']) 23 | op.drop_constraint('unq_buildplan_build', 'buildplan') 24 | -------------------------------------------------------------------------------- /migrations/versions/45e1cfacfc7d_task_parent_id_is_op.py: -------------------------------------------------------------------------------- 1 | """Task.parent_id is optional 2 | 3 | Revision ID: 45e1cfacfc7d 4 | Revises: 26dbd6ff063c 5 | Create Date: 2014-01-13 16:23:03.890537 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '45e1cfacfc7d' 11 | down_revision = '26dbd6ff063c' 12 | 13 | from alembic import op 14 | from sqlalchemy.dialects import postgresql 15 | 16 | 17 | def upgrade(): 18 | op.alter_column('task', 'parent_id', existing_type=postgresql.UUID(), 19 | nullable=True) 20 | 21 | 22 | def downgrade(): 23 | op.alter_column('task', 'parent_id', existing_type=postgresql.UUID(), 24 | nullable=False) 25 | -------------------------------------------------------------------------------- /tests/changes/api/validators/test_basic.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from changes.api.validators.basic import bounded_integer 3 | 4 | 5 | def test_bounded_integer(): 6 | validator = bounded_integer(0, 10) 7 | with pytest.raises(ValueError): 8 | validator("11") 9 | 10 | with pytest.raises(ValueError): 11 | validator("4.5") 12 | 13 | with pytest.raises(ValueError): 14 | validator("-3") 15 | 16 | with pytest.raises(ValueError): 17 | validator("") 18 | 19 | with pytest.raises(ValueError): 20 | validator("10000") 21 | 22 | # No error expeceted 23 | validator("0") 24 | validator("2") 25 | validator("9") 26 | validator("10") 27 | -------------------------------------------------------------------------------- /changes/api/serializer/models/patch.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from changes.api.serializer import Crumbler, register 4 | from changes.models.patch import Patch 5 | from changes.utils.http import build_web_uri 6 | 7 | 8 | @register(Patch) 9 | class PatchCrumbler(Crumbler): 10 | def crumble(self, instance, attrs): 11 | return { 12 | 'id': instance.id.hex, 13 | 'diff': instance.diff, 14 | 'link': build_web_uri('/patches/{0}/'.format(instance.id.hex)), 15 | 'parentRevision': { 16 | 'sha': instance.parent_revision_sha, 17 | }, 18 | 'dateCreated': instance.date_created, 19 | } 20 | -------------------------------------------------------------------------------- /tests/changes/api/test_cluster_nodes.py: -------------------------------------------------------------------------------- 1 | from changes.testutils import APITestCase 2 | 3 | 4 | class ClusterNodesTest(APITestCase): 5 | def test_simple(self): 6 | cluster_1 = self.create_cluster(label='bar') 7 | node_1 = self.create_node(cluster=cluster_1, label='foo') 8 | node_2 = self.create_node(cluster=cluster_1, label='test') 9 | 10 | path = '/api/0/clusters/{0}/nodes/'.format(cluster_1.id.hex) 11 | 12 | resp = self.client.get(path) 13 | assert resp.status_code == 200 14 | data = self.unserialize(resp) 15 | assert len(data) == 2 16 | assert data[0]['id'] == node_1.id.hex 17 | assert data[1]['id'] == node_2.id.hex 18 | -------------------------------------------------------------------------------- /changes/utils/cache.py: -------------------------------------------------------------------------------- 1 | class memoize(object): 2 | """ 3 | Memoize the result of a property call. 4 | 5 | >>> class A(object): 6 | >>> @memoize 7 | >>> def func(self): 8 | >>> return 'foo' 9 | """ 10 | 11 | def __init__(self, func): 12 | self.__name__ = func.__name__ 13 | self.__module__ = func.__module__ 14 | self.__doc__ = func.__doc__ 15 | self.func = func 16 | 17 | def __get__(self, obj, type=None): 18 | if obj is None: 19 | return self 20 | d, n = vars(obj), self.__name__ 21 | if n not in d: 22 | value = self.func(obj) 23 | d[n] = value 24 | return value 25 | -------------------------------------------------------------------------------- /migrations/versions/15f131d88adf_remove_patch_project.py: -------------------------------------------------------------------------------- 1 | """Remove patch.project 2 | 3 | Revision ID: 15f131d88adf 4 | Revises: 1c5907e309f1 5 | Create Date: 2014-06-10 14:36:35.068328 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '15f131d88adf' 11 | down_revision = '1c5907e309f1' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | from sqlalchemy.dialects import postgresql 16 | 17 | 18 | def upgrade(): 19 | op.drop_column('patch', 'project_id') 20 | 21 | 22 | def downgrade(): 23 | op.create_index('idx_patch_project_id', 'patch', ['project_id'], unique=False) 24 | op.add_column('patch', sa.Column('project_id', postgresql.UUID(), nullable=False)) 25 | -------------------------------------------------------------------------------- /changes/api/auth_index.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, unicode_literals 2 | 3 | from changes.api.auth import get_current_user 4 | from changes.api.base import APIView 5 | 6 | 7 | class AuthIndexAPIView(APIView): 8 | def get(self): 9 | """ 10 | Return information on the currently authenticated user. 11 | """ 12 | user = get_current_user() 13 | 14 | if user is None: 15 | context = { 16 | 'authenticated': False, 17 | } 18 | else: 19 | context = { 20 | 'authenticated': True, 21 | 'user': user, 22 | } 23 | 24 | return self.respond(context) 25 | -------------------------------------------------------------------------------- /migrations/versions/1ad857c78a0d_family_id_build_id.py: -------------------------------------------------------------------------------- 1 | """*.family_id => build_id 2 | 3 | Revision ID: 1ad857c78a0d 4 | Revises: 545ba163595 5 | Create Date: 2013-12-26 01:57:32.211057 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '1ad857c78a0d' 11 | down_revision = '545ba163595' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute('ALTER TABLE job RENAME COLUMN family_id TO build_id') 18 | op.execute('ALTER TABLE jobplan RENAME COLUMN family_id TO build_id') 19 | 20 | 21 | def downgrade(): 22 | op.execute('ALTER TABLE job RENAME COLUMN build_id TO family_id') 23 | op.execute('ALTER TABLE jobplan RENAME COLUMN build_id TO family_id') 24 | -------------------------------------------------------------------------------- /migrations/versions/3df65ebfa27e_remove_patch_label_and_message.py: -------------------------------------------------------------------------------- 1 | """Remove Patch label and message 2 | 3 | Revision ID: 3df65ebfa27e 4 | Revises: 315e787e94bf 5 | Create Date: 2014-05-06 18:41:16.245897 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3df65ebfa27e' 11 | down_revision = '315e787e94bf' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.drop_column('patch', 'message') 19 | op.drop_column('patch', 'label') 20 | 21 | 22 | def downgrade(): 23 | op.add_column('patch', sa.Column('label', sa.VARCHAR(length=64), nullable=False)) 24 | op.add_column('patch', sa.Column('message', sa.TEXT(), nullable=True)) 25 | -------------------------------------------------------------------------------- /changes/api/job_artifact_index.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from sqlalchemy.orm import joinedload 4 | 5 | from changes.api.base import APIView 6 | from changes.models.artifact import Artifact 7 | from changes.models.job import Job 8 | 9 | 10 | class JobArtifactIndexAPIView(APIView): 11 | def get(self, job_id): 12 | job = Job.query.get(job_id) 13 | if job is None: 14 | return '', 404 15 | 16 | queryset = Artifact.query.options( 17 | joinedload('step') 18 | ).filter( 19 | Artifact.job_id == job.id, 20 | ).order_by( 21 | Artifact.name.asc(), 22 | ) 23 | 24 | return self.paginate(queryset) 25 | -------------------------------------------------------------------------------- /migrations/versions/1cb3db431e29_add_build_collection_id.py: -------------------------------------------------------------------------------- 1 | """Add Build.collection_id 2 | 3 | Revision ID: 1cb3db431e29 4 | Revises: 3aed22af8f4f 5 | Create Date: 2014-12-10 22:00:33.463247 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '1cb3db431e29' 11 | down_revision = '3aed22af8f4f' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('build', sa.Column('collection_id', sa.GUID(), nullable=True)) 19 | op.create_index('idx_build_collection_id', 'build', ['collection_id']) 20 | 21 | 22 | def downgrade(): 23 | op.drop_index('idx_build_collection_id', 'build') 24 | op.drop_column('build', 'collection_id') 25 | -------------------------------------------------------------------------------- /migrations/versions/290a71847cd7_add_jobstep_cluster.py: -------------------------------------------------------------------------------- 1 | """add jobstep cluster 2 | 3 | Revision ID: 290a71847cd7 4 | Revises: 41c7ef24fd4c 5 | Create Date: 2016-01-11 14:07:49.879954 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '290a71847cd7' 11 | down_revision = '41c7ef24fd4c' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('jobstep', sa.Column('cluster', sa.String(length=128), 19 | nullable=True)) 20 | op.create_index('idx_jobstep_cluster', 'jobstep', ['cluster']) 21 | 22 | 23 | def downgrade(): 24 | # also drops the index 25 | op.drop_column('jobstep', 'cluster') 26 | -------------------------------------------------------------------------------- /migrations/versions/57e24a9f2290_log_build_id_job_id.py: -------------------------------------------------------------------------------- 1 | """Log*.build_id => job_id 2 | 3 | Revision ID: 57e24a9f2290 4 | Revises: 35af40cebcde 5 | Create Date: 2013-12-26 01:03:06.812123 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '57e24a9f2290' 11 | down_revision = '35af40cebcde' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute('ALTER TABLE logsource RENAME COLUMN build_id TO job_id') 18 | op.execute('ALTER TABLE logchunk RENAME COLUMN build_id TO job_id') 19 | 20 | 21 | def downgrade(): 22 | op.execute('ALTER TABLE logsource RENAME COLUMN job_id TO build_id') 23 | op.execute('ALTER TABLE logchunk RENAME COLUMN job_id TO build_id') 24 | -------------------------------------------------------------------------------- /static/vendor/requirejs/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "requirejs", 3 | "version": "2.1.22", 4 | "ignore": [], 5 | "homepage": "http://requirejs.org", 6 | "authors": [ 7 | "jrburke.com" 8 | ], 9 | "description": "A file and module loader for JavaScript", 10 | "main": "require.js", 11 | "keywords": [ 12 | "AMD" 13 | ], 14 | "license": [ 15 | "BSD-3-Clause", 16 | "MIT" 17 | ], 18 | "_release": "2.1.22", 19 | "_resolution": { 20 | "type": "version", 21 | "tag": "2.1.22", 22 | "commit": "9cd0b99417eac61e890d11c5119f2e45e752c999" 23 | }, 24 | "_source": "https://github.com/jrburke/requirejs-bower.git", 25 | "_target": "~2.1.11", 26 | "_originalSource": "requirejs" 27 | } -------------------------------------------------------------------------------- /tests/changes/api/serializer/models/test_project.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from uuid import UUID 3 | 4 | from changes.api.serializer import serialize 5 | from changes.models.project import Project 6 | 7 | 8 | def test_simple(): 9 | project = Project( 10 | id=UUID(hex='33846695b2774b29a71795a009e8168a'), 11 | slug='hello-world', 12 | name='Hello world', 13 | date_created=datetime(2013, 9, 19, 22, 15, 22), 14 | ) 15 | result = serialize(project) 16 | assert result['name'] == 'Hello world' 17 | assert result['id'] == '33846695b2774b29a71795a009e8168a' 18 | assert result['slug'] == 'hello-world' 19 | assert result['dateCreated'] == '2013-09-19T22:15:22' 20 | -------------------------------------------------------------------------------- /tests/changes/api/test_node_from_hostname.py: -------------------------------------------------------------------------------- 1 | from changes.testutils import APITestCase 2 | 3 | 4 | class NodeFromHostnameTest(APITestCase): 5 | def test_simple(self): 6 | node = self.create_node(label='ip-127-0-0-1') 7 | path = '/api/0/nodes/hostname/{0}/'.format(node.label) 8 | 9 | resp = self.client.get(path) 10 | assert resp.status_code == 200 11 | data = self.unserialize(resp) 12 | assert data['id'] == node.id.hex 13 | 14 | def test_not_found(self): 15 | self.create_node(label='ip-127-0-0-1') 16 | path = '/api/0/nodes/hostname/{0}/'.format('ip-0-0-0-0') 17 | 18 | resp = self.client.get(path) 19 | assert resp.status_code == 404 20 | -------------------------------------------------------------------------------- /changes/api/serializer/models/project.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.project import Project 3 | from changes.utils.http import build_web_uri 4 | 5 | 6 | @register(Project) 7 | class ProjectCrumbler(Crumbler): 8 | def crumble(self, instance, attrs): 9 | return { 10 | 'id': instance.id.hex, 11 | 'slug': instance.slug, 12 | 'name': instance.name, 13 | 'repository': { 14 | 'id': instance.repository_id, 15 | }, 16 | 'status': instance.status, 17 | 'dateCreated': instance.date_created, 18 | 'link': build_web_uri('/projects/{0}/'.format(instance.slug)), 19 | } 20 | -------------------------------------------------------------------------------- /changes/api/validators/datetime.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from datetime import datetime 4 | 5 | import logging 6 | 7 | # We appear to be hitting https://bugs.python.org/issue7980, but by using 8 | # strptime early on, the race should be avoided. 9 | datetime.strptime("", "") 10 | 11 | 12 | class ISODatetime(object): 13 | def __call__(self, value): 14 | # type: (str) -> datetime 15 | try: 16 | return datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ') 17 | except Exception: 18 | logging.exception("Failed to parse datetime: %s", value) 19 | raise ValueError('Datetime was not parseable. Expected ISO 8601 with timezone: YYYY-MM-DDTHH:MM:SS.mmmmmmZ') 20 | -------------------------------------------------------------------------------- /changes/utils/slugs.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unicodedata 3 | 4 | # Extra characters outside of alphanumerics that we'll allow. 5 | SLUG_OK = '-_~' 6 | 7 | 8 | def slugify(s, ok=SLUG_OK, lower=True, spaces=False): 9 | # L and N signify letter/number. 10 | # http://www.unicode.org/reports/tr44/tr44-4.html#GC_Values_Table 11 | rv = [] 12 | for c in unicodedata.normalize('NFKC', unicode(s)): 13 | cat = unicodedata.category(c)[0] 14 | if cat in 'LN' or c in ok: 15 | rv.append(c) 16 | if cat == 'Z': # space 17 | rv.append(' ') 18 | new = ''.join(rv).strip() 19 | if not spaces: 20 | new = re.sub('[-\s]+', '-', new) 21 | return new.lower() if lower else new 22 | -------------------------------------------------------------------------------- /migrations/versions/3265d2120c82_add_testcase_step_id.py: -------------------------------------------------------------------------------- 1 | """Add TestCase.step_id 2 | 3 | Revision ID: 3265d2120c82 4 | Revises: 21c9439330f 5 | Create Date: 2014-04-02 15:26:58.967387 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3265d2120c82' 11 | down_revision = '21c9439330f' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('test', sa.Column('step_id', sa.GUID(), nullable=True)) 19 | op.create_foreign_key('test_step_id_fkey', 'test', 'jobstep', ['step_id'], ['id'], ondelete='CASCADE') 20 | op.create_index('idx_test_step_id', 'test', ['step_id']) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column('test', 'step_id') 25 | -------------------------------------------------------------------------------- /migrations/versions/3e85891e90bb_add_jobstep_replacement_id.py: -------------------------------------------------------------------------------- 1 | """Add jobstep replacement_id 2 | 3 | Revision ID: 3e85891e90bb 4 | Revises: 291978a5d5e4 5 | Create Date: 2015-08-31 16:19:46.669260 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3e85891e90bb' 11 | down_revision = '291978a5d5e4' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('jobstep', sa.Column('replacement_id', sa.GUID(), 19 | sa.ForeignKey('jobstep.id', ondelete="CASCADE"), 20 | unique=True, nullable=True)) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column('jobstep', 'replacement_id') 25 | -------------------------------------------------------------------------------- /tests/changes/api/test_build_mark_seen.py: -------------------------------------------------------------------------------- 1 | from changes.models.buildseen import BuildSeen 2 | from changes.testutils import APITestCase 3 | 4 | 5 | class BuildMarkSeenTest(APITestCase): 6 | def test_simple(self): 7 | project = self.create_project() 8 | build = self.create_build(project=project) 9 | 10 | self.login_default() 11 | 12 | path = '/api/0/builds/{0}/mark_seen/'.format(build.id.hex) 13 | resp = self.client.post(path) 14 | 15 | assert resp.status_code == 200 16 | 17 | buildseen = BuildSeen.query.filter( 18 | BuildSeen.user_id == self.default_user.id, 19 | BuildSeen.build_id == build.id, 20 | ).first() 21 | 22 | assert buildseen 23 | -------------------------------------------------------------------------------- /tests/changes/api/test_patch_details.py: -------------------------------------------------------------------------------- 1 | from changes.testutils import APITestCase 2 | 3 | 4 | class PatchDetailsTest(APITestCase): 5 | def test_simple(self): 6 | patch = self.create_patch() 7 | 8 | path = '/api/0/patches/{0}/'.format(patch.id.hex) 9 | 10 | resp = self.client.get(path) 11 | assert resp.status_code == 200 12 | data = self.unserialize(resp) 13 | assert data['id'] == patch.id.hex 14 | 15 | def test_raw(self): 16 | patch = self.create_patch() 17 | 18 | path = '/api/0/patches/{0}/?raw=1'.format(patch.id.hex) 19 | 20 | resp = self.client.get(path) 21 | assert resp.status_code == 200 22 | assert resp.data.decode('utf-8') == patch.diff 23 | -------------------------------------------------------------------------------- /tests/changes/api/test_repository_project_index.py: -------------------------------------------------------------------------------- 1 | from changes.testutils import APITestCase 2 | 3 | 4 | class RepositoryProjectListTest(APITestCase): 5 | def test_simple(self): 6 | repo = self.create_repo(url='https://example.com/bar') 7 | project = self.create_project(repository=repo, slug='foo') 8 | repo_2 = self.create_repo(url='https://example.com/foo') 9 | self.create_project(repository=repo_2, slug='bar') 10 | 11 | path = '/api/0/repositories/{0}/projects/'.format(repo.id) 12 | 13 | resp = self.client.get(path) 14 | assert resp.status_code == 200 15 | data = self.unserialize(resp) 16 | assert len(data) == 1 17 | assert data[0]['id'] == project.id.hex 18 | -------------------------------------------------------------------------------- /migrations/versions/12569fada93_require_plan_project_id.py: -------------------------------------------------------------------------------- 1 | """Require Plan.project_id 2 | 3 | Revision ID: 12569fada93 4 | Revises: 4c3f2a63b7a7 5 | Create Date: 2014-10-14 11:23:49.892957 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '12569fada93' 11 | down_revision = '4c3f2a63b7a7' 12 | 13 | from alembic import op 14 | from sqlalchemy.dialects import postgresql 15 | 16 | 17 | def upgrade(): 18 | op.alter_column('plan', 'project_id', 19 | existing_type=postgresql.UUID(), 20 | nullable=False) 21 | 22 | 23 | def downgrade(): 24 | op.alter_column('plan', 'project_id', 25 | existing_type=postgresql.UUID(), 26 | nullable=True) 27 | -------------------------------------------------------------------------------- /changes/models/itemsequence.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer 2 | 3 | from changes.config import db 4 | from changes.db.types.guid import GUID 5 | 6 | 7 | class ItemSequence(db.Model): 8 | """ 9 | Used to hold counters for autoincrement-style sequence number generation. 10 | In each row, value is the last sequence number returned for the 11 | corresponding parent. 12 | 13 | The table is used via the next_item_value database function and not used in 14 | the python codebase. 15 | """ 16 | __tablename__ = 'itemsequence' 17 | 18 | parent_id = Column(GUID, nullable=False, primary_key=True) 19 | value = Column(Integer, default=0, server_default='0', nullable=False, 20 | primary_key=True) 21 | -------------------------------------------------------------------------------- /tests/changes/test_config.py: -------------------------------------------------------------------------------- 1 | from mock import patch 2 | 3 | from changes.config import db 4 | from changes.models.user import User 5 | from changes.testutils import TestCase 6 | 7 | 8 | class TestDBTransactionTracking(TestCase): 9 | 10 | @patch('logging.warning') 11 | def test_txn_timing(self, mock_warning): 12 | with patch('time.time', return_value=10): 13 | user_emails = list(db.session.query(User.email)) 14 | 15 | assert 10 == db.session.info['txn_start_time'] 16 | 17 | with patch('time.time', return_value=15): 18 | db.session.commit() 19 | 20 | assert 'txn_start_time' not in db.session.info 21 | call_args = mock_warning.call_args[0] 22 | assert call_args[2] == 5000 23 | -------------------------------------------------------------------------------- /webapp/css/bootstrap_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "vars": {}, 3 | "css": [ 4 | "navs.less", 5 | "breadcrumbs.less", 6 | "pagination.less", 7 | "pager.less", 8 | "labels.less", 9 | "badges.less", 10 | "jumbotron.less", 11 | "thumbnails.less", 12 | "alerts.less", 13 | "progress-bars.less", 14 | "media.less", 15 | "list-group.less", 16 | "panels.less", 17 | "responsive-embed.less", 18 | "wells.less", 19 | "close.less", 20 | "component-animations.less", 21 | "dropdowns.less", 22 | "tooltip.less", 23 | "popovers.less", 24 | "modals.less", 25 | "carousel.less" 26 | ], 27 | "js": [], 28 | "customizerUrl": "http://getbootstrap.com/customize/?id=750b967675321de6e54c" 29 | } 30 | -------------------------------------------------------------------------------- /changes/debug/reports/build.py: -------------------------------------------------------------------------------- 1 | import toronado 2 | 3 | from flask import render_template, request 4 | from flask.views import MethodView 5 | from jinja2 import Markup 6 | 7 | from changes.models.project import Project 8 | from changes.reports.build import BuildReport 9 | 10 | 11 | class BuildReportMailView(MethodView): 12 | def get(self, path=''): 13 | projects = Project.query.all() 14 | 15 | report = BuildReport(projects) 16 | 17 | context = report.generate(days=int(request.args.get('days', 7))) 18 | 19 | html_content = Markup(toronado.from_string( 20 | render_template('email/build_report.html', **context) 21 | )) 22 | 23 | return render_template('debug/email.html', html_content=html_content) 24 | -------------------------------------------------------------------------------- /migrations/versions/2f3ba1e84a6f_update_source_constr.py: -------------------------------------------------------------------------------- 1 | """Update Source constraints 2 | 3 | Revision ID: 2f3ba1e84a6f 4 | Revises: cb99fdfb903 5 | Create Date: 2013-12-23 14:46:38.140915 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2f3ba1e84a6f' 11 | down_revision = 'cb99fdfb903' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute('CREATE UNIQUE INDEX unq_source_revision ON source (repository_id, revision_sha) WHERE patch_id IS NULL') 18 | op.execute('CREATE UNIQUE INDEX unq_source_patch_id ON source (patch_id) WHERE patch_id IS NOT NULL') 19 | 20 | 21 | def downgrade(): 22 | op.drop_constraint('unq_source_revision', 'source') 23 | op.drop_constraint('unq_source_patch_id', 'source') 24 | -------------------------------------------------------------------------------- /migrations/versions/3bef10ab5088_add_various_testgrou.py: -------------------------------------------------------------------------------- 1 | """Add various TestGroup indexes 2 | 3 | Revision ID: 3bef10ab5088 4 | Revises: fd1a52fe89f 5 | Create Date: 2013-11-04 17:10:52.057285 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3bef10ab5088' 11 | down_revision = 'fd1a52fe89f' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_index('idx_testgroup_project_id', 'testgroup', ['project_id']) 18 | op.create_index('idx_testgroup_suite_id', 'testgroup', ['suite_id']) 19 | op.create_index('idx_testgroup_parent_id', 'testgroup', ['parent_id']) 20 | 21 | 22 | def downgrade(): 23 | ### commands auto generated by Alembic - please adjust! ### 24 | pass 25 | ### end Alembic commands ### 26 | -------------------------------------------------------------------------------- /migrations/versions/52bcea82b482_remove_patch_url.py: -------------------------------------------------------------------------------- 1 | """Remove Patch.url 2 | 3 | Revision ID: 52bcea82b482 4 | Revises: 346c011ca77a 5 | Create Date: 2013-11-11 17:17:49.008228 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '52bcea82b482' 11 | down_revision = '346c011ca77a' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.drop_column('patch', u'url') 20 | ### end Alembic commands ### 21 | 22 | 23 | def downgrade(): 24 | ### commands auto generated by Alembic - please adjust! ### 25 | op.add_column('patch', sa.Column(u'url', sa.VARCHAR(length=200), nullable=True)) 26 | ### end Alembic commands ### 27 | -------------------------------------------------------------------------------- /tests/changes/backends/jenkins/fixtures/GET/node_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | server-ubuntu-10.04 (ami-746cf244) (i-836023b7) 4 | Swarm slave from 127.0.0.1 : null 5 | /home/ubuntu 6 | 1 7 | NORMAL 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | jenkins-slave 17 | 18 | -------------------------------------------------------------------------------- /migrations/versions/3bf1066f4935_add_label_in_testmessage.py: -------------------------------------------------------------------------------- 1 | """Add label in testmessage 2 | 3 | Revision ID: 3bf1066f4935 4 | Revises: 51775a13339d 5 | Create Date: 2016-06-30 14:12:07.405213 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3bf1066f4935' 11 | down_revision = '51775a13339d' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('testmessage', sa.Column('label', sa.Text(), nullable=True)) 19 | testmessage = sa.table('testmessage', sa.column('label')) 20 | op.execute(testmessage.update().values(label='test-message')) 21 | op.alter_column('testmessage', 'label', nullable=False) 22 | 23 | 24 | def downgrade(): 25 | op.drop_column('testmessage', 'label') 26 | -------------------------------------------------------------------------------- /migrations/versions/47e23df5a7ed_add_testgroup_result.py: -------------------------------------------------------------------------------- 1 | """Add TestGroup.result 2 | 3 | Revision ID: 47e23df5a7ed 4 | Revises: 520eba1ce36e 5 | Create Date: 2013-11-05 14:01:08.310862 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '47e23df5a7ed' 11 | down_revision = '520eba1ce36e' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('testgroup', sa.Column('result', sa.Enum(), nullable=True)) 20 | ### end Alembic commands ### 21 | 22 | 23 | def downgrade(): 24 | ### commands auto generated by Alembic - please adjust! ### 25 | op.drop_column('testgroup', 'result') 26 | ### end Alembic commands ### 27 | -------------------------------------------------------------------------------- /static/vendor/react/react-dom.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ReactDOM v15.0.2 3 | * 4 | * Copyright 2013-present, Facebook, Inc. 5 | * All rights reserved. 6 | * 7 | * This source code is licensed under the BSD-style license found in the 8 | * LICENSE file in the root directory of this source tree. An additional grant 9 | * of patent rights can be found in the PATENTS file in the same directory. 10 | * 11 | */ 12 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e(require("react"));else if("function"==typeof define&&define.amd)define(["react"],e);else{var f;f="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,f.ReactDOM=e(f.React)}}(function(e){return e.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED}); -------------------------------------------------------------------------------- /migrations/versions/3cf79ad9bde_add_snapshot_plan_id_to_plan.py: -------------------------------------------------------------------------------- 1 | """add_snapshot_plan_id_to_plan 2 | 3 | Revision ID: 3cf79ad9bde 4 | Revises: 26fa678dcc64 5 | Create Date: 2015-07-08 10:24:29.696113 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3cf79ad9bde' 11 | down_revision = '26fa678dcc64' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('plan', sa.Column('snapshot_plan_id', sa.GUID(), nullable=True)) 19 | op.create_foreign_key('plan_snapshot_plan_id_fkey', 'plan', 'plan', ['snapshot_plan_id'], ['id'], ondelete='SET NULL') 20 | 21 | 22 | def downgrade(): 23 | op.drop_constraint('plan_snapshot_plan_id_fkey', 'plan') 24 | op.drop_column('plan', 'snapshot_plan_id') 25 | -------------------------------------------------------------------------------- /tests/changes/api/test_project_source_build_index.py: -------------------------------------------------------------------------------- 1 | from changes.testutils import APITestCase 2 | 3 | 4 | class ProjectSourceBuildIndexTest(APITestCase): 5 | def test_simple(self): 6 | project = self.create_project() 7 | source = self.create_source(project) 8 | build1 = self.create_build(project, source=source) 9 | build2 = self.create_build(project, source=source) 10 | path = '/api/0/projects/{0}/sources/{1}/builds/'.format( 11 | project.id.hex, source.id.hex) 12 | 13 | resp = self.client.get(path) 14 | assert resp.status_code == 200 15 | data = self.unserialize(resp) 16 | assert len(data) == 2 17 | assert data[0]['id'] == build2.id.hex 18 | assert data[1]['id'] == build1.id.hex 19 | -------------------------------------------------------------------------------- /migrations/versions/14491da59392_add_build_revision_s.py: -------------------------------------------------------------------------------- 1 | """Add Build.revision-sha 2 | 3 | Revision ID: 14491da59392 4 | Revises: 32274e97552 5 | Create Date: 2013-10-29 11:43:47.367799 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '14491da59392' 11 | down_revision = '32274e97552' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('build', sa.Column('revision_sha', sa.String(length=40), nullable=True)) 20 | ### end Alembic commands ### 21 | 22 | 23 | def downgrade(): 24 | ### commands auto generated by Alembic - please adjust! ### 25 | op.drop_column('build', 'revision_sha') 26 | ### end Alembic commands ### 27 | -------------------------------------------------------------------------------- /migrations/versions/fd1a52fe89f_add_various_test_ind.py: -------------------------------------------------------------------------------- 1 | """Add various TestCase indexes 2 | 3 | Revision ID: fd1a52fe89f 4 | Revises: 5177cfff57d7 5 | Create Date: 2013-11-04 17:03:03.005904 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = 'fd1a52fe89f' 11 | down_revision = '5177cfff57d7' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | # TestCase 18 | op.create_index('idx_test_project_id', 'test', ['project_id']) 19 | op.create_index('idx_test_suite_id', 'test', ['suite_id']) 20 | op.create_unique_constraint('unq_test_key', 'test', ['build_id', 'suite_id', 'label_sha']) 21 | 22 | 23 | def downgrade(): 24 | ### commands auto generated by Alembic - please adjust! ### 25 | pass 26 | ### end Alembic commands ### 27 | -------------------------------------------------------------------------------- /changes/api/task_index.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from flask_restful.reqparse import RequestParser 4 | 5 | from changes.api.base import APIView 6 | from changes.models.task import Task 7 | 8 | import uuid 9 | 10 | 11 | class TaskIndexAPIView(APIView): 12 | get_parser = RequestParser() 13 | 14 | # If specified, the results will be limited to Tasks associated with this object id. 15 | get_parser.add_argument('object_id', type=uuid.UUID, default=None) 16 | 17 | def get(self): 18 | args = self.get_parser.parse_args() 19 | 20 | queryset = Task.query.order_by(Task.date_created.desc()) 21 | if args.object_id: 22 | queryset = queryset.filter(Task.task_id == args.object_id) 23 | 24 | return self.paginate(queryset) 25 | -------------------------------------------------------------------------------- /tests/changes/api/test_build_message_index.py: -------------------------------------------------------------------------------- 1 | from changes.testutils import APITestCase 2 | 3 | 4 | class BuildMessageIndexTestCase(APITestCase): 5 | path = '/api/0/builds/{build_id}/messages/' 6 | 7 | def test_correct(self): 8 | build = self.create_build(self.create_project()) 9 | message1 = self.create_build_message(build, text="Test message") 10 | message2 = self.create_build_message(build, text="Test message") 11 | message3 = self.create_build_message(build, text="Test message") 12 | 13 | resp = self.client.get(self.path.format(build_id=build.id.hex)) 14 | assert resp.status_code == 200 15 | data = self.unserialize(resp) 16 | 17 | assert [m['id'] for m in data] == [message1.id.hex, message2.id.hex, message3.id.hex] 18 | -------------------------------------------------------------------------------- /changes/api/build_mark_seen.py: -------------------------------------------------------------------------------- 1 | from changes.api.auth import get_current_user 2 | from changes.api.base import APIView 3 | from changes.db.utils import try_create 4 | from changes.models.build import Build 5 | from changes.models.buildseen import BuildSeen 6 | 7 | 8 | class BuildMarkSeenAPIView(APIView): 9 | def post(self, build_id): 10 | build = Build.query.get(build_id) 11 | if build is None: 12 | return '', 404 13 | 14 | user = get_current_user() 15 | if user is None: 16 | # don't do anything if they aren't logged in 17 | return self.respond({}) 18 | 19 | try_create(BuildSeen, where={ 20 | 'build_id': build.id, 21 | 'user_id': user.id, 22 | }) 23 | 24 | return self.respond({}) 25 | -------------------------------------------------------------------------------- /static/vendor/react/react-dom-server.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ReactDOMServer v15.0.2 3 | * 4 | * Copyright 2013-present, Facebook, Inc. 5 | * All rights reserved. 6 | * 7 | * This source code is licensed under the BSD-style license found in the 8 | * LICENSE file in the root directory of this source tree. An additional grant 9 | * of patent rights can be found in the PATENTS file in the same directory. 10 | * 11 | */ 12 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e(require("react"));else if("function"==typeof define&&define.amd)define(["react"],e);else{var f;f="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,f.ReactDOMServer=e(f.React)}}(function(e){return e.__SECRET_DOM_SERVER_DO_NOT_USE_OR_YOU_WILL_BE_FIRED}); -------------------------------------------------------------------------------- /tests/changes/api/serializer/models/test_buildmessage.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from changes.api.serializer import serialize 4 | from changes.testutils.cases import TestCase 5 | 6 | 7 | class BuildMessageTestCase(TestCase): 8 | 9 | def test_correct(self): 10 | build = self.create_build(self.create_project()) 11 | message = self.create_build_message( 12 | build, 13 | text="Test message", 14 | date_created=datetime(2013, 9, 19, 22, 15, 22), 15 | ) 16 | 17 | result = serialize(message) 18 | assert result['id'] == message.id.hex 19 | assert result['build']['id'] == message.build_id.hex 20 | assert result['text'] == message.text 21 | assert result['dateCreated'] == '2013-09-19T22:15:22' 22 | -------------------------------------------------------------------------------- /migrations/versions/4640ecd97c82_add_testgroup_num_le.py: -------------------------------------------------------------------------------- 1 | """Add TestGroup.num_leaves 2 | 3 | Revision ID: 4640ecd97c82 4 | Revises: 48a922151dd4 5 | Create Date: 2013-11-08 13:11:18.802332 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4640ecd97c82' 11 | down_revision = '48a922151dd4' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('testgroup', sa.Column('num_leaves', sa.Integer(), nullable=False, server_default='0')) 20 | ### end Alembic commands ### 21 | 22 | 23 | def downgrade(): 24 | ### commands auto generated by Alembic - please adjust! ### 25 | op.drop_column('testgroup', 'num_leaves') 26 | ### end Alembic commands ### 27 | -------------------------------------------------------------------------------- /tests/changes/api/test_task_index.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from changes.testutils import APITestCase 4 | 5 | 6 | class TaskIndexTest(APITestCase): 7 | def test_simple(self): 8 | task_1 = self.create_task( 9 | task_name='example', 10 | date_created=datetime(2013, 9, 19, 22, 15, 24), 11 | ) 12 | task_2 = self.create_task( 13 | task_name='example', 14 | date_created=datetime(2013, 9, 20, 22, 15, 24), 15 | ) 16 | 17 | path = '/api/0/tasks/' 18 | 19 | resp = self.client.get(path) 20 | assert resp.status_code == 200 21 | data = self.unserialize(resp) 22 | assert len(data) == 2 23 | assert data[0]['id'] == task_2.id.hex 24 | assert data[1]['id'] == task_1.id.hex 25 | -------------------------------------------------------------------------------- /migrations/versions/139e272152de_index_jobphase.py: -------------------------------------------------------------------------------- 1 | """Index JobPhase 2 | 3 | Revision ID: 139e272152de 4 | Revises: 2f4637448764 5 | Create Date: 2014-01-02 22:03:22.957636 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '139e272152de' 11 | down_revision = '2f4637448764' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_index('idx_jobphase_job_id', 'jobphase', ['job_id']) 18 | op.create_index('idx_jobphase_project_id', 'jobphase', ['project_id']) 19 | op.create_index('idx_jobphase_repository_id', 'jobphase', ['repository_id']) 20 | 21 | 22 | def downgrade(): 23 | op.drop_index('idx_jobphase_job_id', 'jobphase') 24 | op.drop_index('idx_jobphase_project_id', 'jobphase') 25 | op.drop_index('idx_jobphase_repository_id', 'jobphase') 26 | -------------------------------------------------------------------------------- /static/vendor/classnames/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "classnames", 3 | "version": "2.2.3", 4 | "description": "A simple utility for conditionally joining classNames together", 5 | "main": [ 6 | "index.js", 7 | "bind.js", 8 | "dedupe.js" 9 | ], 10 | "homepage": "https://github.com/JedWatson/classnames", 11 | "authors": [ 12 | "Jed Watson" 13 | ], 14 | "moduleType": [ 15 | "amd", 16 | "globals", 17 | "node" 18 | ], 19 | "keywords": [ 20 | "react", 21 | "css", 22 | "classes", 23 | "classname", 24 | "classnames", 25 | "util", 26 | "utility" 27 | ], 28 | "license": "MIT", 29 | "ignore": [ 30 | ".editorconfig", 31 | ".gitignore", 32 | "gulpfile.js", 33 | "package.json", 34 | "node_modules", 35 | "tests.js" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /migrations/versions/4d5d239d53b4_set_on_delete_cascad.py: -------------------------------------------------------------------------------- 1 | """Set ON DELETE CASCADE on TestSuite.* 2 | 3 | Revision ID: 4d5d239d53b4 4 | Revises: 501983249c94 5 | Create Date: 2013-12-23 16:14:08.812850 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4d5d239d53b4' 11 | down_revision = '501983249c94' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.drop_constraint('testsuite_project_id_fkey', 'testsuite') 18 | op.create_foreign_key('testsuite_project_id_fkey', 'testsuite', 'project', ['project_id'], ['id'], ondelete='CASCADE') 19 | 20 | op.drop_constraint('testsuite_build_id_fkey', 'testsuite') 21 | op.create_foreign_key('testsuite_build_id_fkey', 'testsuite', 'build', ['build_id'], ['id'], ondelete='CASCADE') 22 | 23 | 24 | def downgrade(): 25 | pass 26 | -------------------------------------------------------------------------------- /changes/lib/build_lib.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import literal 2 | from changes.config import db 3 | from changes.models.build import Build # NOQA 4 | from changes.models.jobplan import JobPlan 5 | from changes.models.option import ItemOption 6 | 7 | 8 | def contains_autogenerated_plan(build): 9 | """Returns true if any of the jobs in this build was created with an 10 | autogenerated jobplan. 11 | """ 12 | # type: (Build)->bool 13 | contains = db.session.query(literal(True)).filter( 14 | ItemOption.query.join( 15 | JobPlan, JobPlan.plan_id == ItemOption.item_id, 16 | ).filter( 17 | ItemOption.name == 'bazel.autogenerate', 18 | ItemOption.value == '1', 19 | JobPlan.build_id == build.id, 20 | ).exists() 21 | ).scalar() 22 | return bool(contains) 23 | -------------------------------------------------------------------------------- /static/vendor/requirejs-babel/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "requirejs-babel", 3 | "version": "0.0.6", 4 | "authors": [ 5 | "Michael " 6 | ], 7 | "description": "An AMD loader plugin for Babel", 8 | "main": "es6.js", 9 | "moduleType": [ 10 | "amd" 11 | ], 12 | "keywords": [ 13 | "es6", 14 | "esnext", 15 | "babel" 16 | ], 17 | "license": "MIT", 18 | "ignore": [ 19 | "demo", 20 | "package.json" 21 | ], 22 | "homepage": "https://github.com/mikach/requirejs-babel", 23 | "_release": "0.0.6", 24 | "_resolution": { 25 | "type": "version", 26 | "tag": "0.0.6", 27 | "commit": "f7b24d4aea0c9003ecef3560da771133e6c1755b" 28 | }, 29 | "_source": "https://github.com/mikach/requirejs-babel.git", 30 | "_target": "0.0.6", 31 | "_originalSource": "requirejs-babel" 32 | } -------------------------------------------------------------------------------- /.arclint: -------------------------------------------------------------------------------- 1 | { 2 | "linters": { 3 | "json": { 4 | "type": "json", 5 | "include": [ 6 | "(^\\.arcconfig$)", 7 | "(^\\.arclint$)", 8 | "(\\.json$)" 9 | ] 10 | }, 11 | "spelling": { 12 | "type": "spelling" 13 | }, 14 | "text": { 15 | "type": "text", 16 | "text.max-line-length": 1000 17 | }, 18 | "python": { 19 | "type": "flake8", 20 | "include": [ 21 | "(^tests/.*\\.py$)", 22 | "(^changes/.*\\.py$)" 23 | ] 24 | }, 25 | "python-linters": { 26 | "type": "script-and-regex", 27 | "script-and-regex.script": "bin/lint", 28 | "script-and-regex.regex": "/^(?Padvice|warning|error):(?P\\d+) (?P.*)$/m", 29 | "include": [ 30 | "(^.*\\.py$)" 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ci/mypy-run: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eux 2 | 3 | # Allow the path to mypy to be specified in the MYPY environment variable, but default to "mypy". 4 | : ${MYPY=mypy} 5 | 6 | # Any paths we need to include in typechecking that are not automatically found (that is, that 7 | # have no '# type:' annotation) 8 | EXTRA_FILES="" 9 | 10 | # Any files with type annotations that should be excluded from typechecking. This is a regular 11 | # expression matched against the filenames. 12 | EXCLUDE="" 13 | 14 | # Find all Python files that are not in the exclude list and which have a '# type:' annotation. 15 | FILES=`find . -type f -name \*.py -print0 \ 16 | | xargs -0 grep -ls '# type:'` 17 | 18 | if [ -n "$EXCLUDE" ]; then 19 | FILES=`echo "$FILES" | egrep -v "$EXCLUDE"` 20 | fi 21 | 22 | ci/run_mypy.py $MYPY --silent-imports --py2 $FILES $EXTRA_FILES 23 | -------------------------------------------------------------------------------- /static/vendor/moment/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moment", 3 | "version": "2.1.0", 4 | "scripts": [ 5 | "moment.js" 6 | ], 7 | "main": "moment.js", 8 | "description": "Parse, validate, manipulate, and display dates in javascript.", 9 | "ignore": [ 10 | ".gitignore", 11 | ".travis.yml", 12 | "composer.json", 13 | "CONTRIBUTING.md", 14 | "ender.js", 15 | "Gruntfile.js", 16 | "package.js", 17 | "package.json", 18 | "test", 19 | "tasks" 20 | ], 21 | "homepage": "https://github.com/moment/moment", 22 | "_release": "2.1.0", 23 | "_resolution": { 24 | "type": "version", 25 | "tag": "2.1.0", 26 | "commit": "05028e20e526b5f38dde3748833e98d7c8e846e8" 27 | }, 28 | "_source": "https://github.com/moment/moment.git", 29 | "_target": "~2.1.0", 30 | "_originalSource": "moment" 31 | } -------------------------------------------------------------------------------- /migrations/versions/306fefe51dc6_set_on_delete_cascad.py: -------------------------------------------------------------------------------- 1 | """Set ON DELETE CASCADE on Project Plan m2m 2 | 3 | Revision ID: 306fefe51dc6 4 | Revises: 37e782a55ca6 5 | Create Date: 2013-12-23 16:44:37.119363 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '306fefe51dc6' 11 | down_revision = '37e782a55ca6' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.drop_constraint('project_plan_plan_id_fkey', 'project_plan') 18 | op.create_foreign_key('project_plan_plan_id_fkey', 'project_plan', 'plan', ['plan_id'], ['id'], ondelete='CASCADE') 19 | 20 | op.drop_constraint('project_plan_project_id_fkey', 'project_plan') 21 | op.create_foreign_key('project_plan_project_id_fkey', 'project_plan', 'project', ['project_id'], ['id'], ondelete='CASCADE') 22 | 23 | 24 | def downgrade(): 25 | pass 26 | -------------------------------------------------------------------------------- /migrations/versions/380d20771802_project_plan.py: -------------------------------------------------------------------------------- 1 | """Project <=> Plan 2 | 3 | Revision ID: 380d20771802 4 | Revises: 1d806848a73f 5 | Create Date: 2013-12-16 14:38:39.941404 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '380d20771802' 11 | down_revision = '1d806848a73f' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.create_table( 19 | 'project_plan', 20 | sa.Column('project_id', sa.GUID(), nullable=False), 21 | sa.Column('plan_id', sa.GUID(), nullable=False), 22 | sa.ForeignKeyConstraint(['plan_id'], ['plan.id'], ), 23 | sa.ForeignKeyConstraint(['project_id'], ['project.id'], ), 24 | sa.PrimaryKeyConstraint('project_id', 'plan_id') 25 | ) 26 | 27 | 28 | def downgrade(): 29 | op.drop_table('project_plan') 30 | -------------------------------------------------------------------------------- /migrations/versions/3be107806e62_make_testartifact_cascade_on_test_delete.py: -------------------------------------------------------------------------------- 1 | """Make testartifact cascade on test delete 2 | 3 | Revision ID: 3be107806e62 4 | Revises: 3bf1066f4935 5 | Create Date: 2016-07-06 18:42:33.893405 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3be107806e62' 11 | down_revision = '3bf1066f4935' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.drop_constraint('testartifact_test_id_fkey', 'testartifact') 19 | op.create_foreign_key('testartifact_test_id_fkey', 'testartifact', 'test', ['test_id'], ['id'], ondelete='CASCADE') 20 | 21 | 22 | def downgrade(): 23 | op.drop_constraint('testartifact_test_id_fkey', 'testartifact') 24 | op.create_foreign_key('testartifact_test_id_fkey', 'testartifact', 'test', ['test_id'], ['id']) 25 | -------------------------------------------------------------------------------- /migrations/versions/5677ef75c712_add_user.py: -------------------------------------------------------------------------------- 1 | """Add User 2 | 3 | Revision ID: 5677ef75c712 4 | Revises: 45e1cfacfc7d 5 | Create Date: 2014-01-15 11:06:25.217408 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '5677ef75c712' 11 | down_revision = '45e1cfacfc7d' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.create_table( 19 | 'user', 20 | sa.Column('id', sa.GUID(), nullable=False), 21 | sa.Column('email', sa.String(length=128), nullable=False), 22 | sa.Column('is_admin', sa.Boolean(), nullable=False), 23 | sa.Column('date_created', sa.DateTime(), nullable=True), 24 | sa.PrimaryKeyConstraint('id'), 25 | sa.UniqueConstraint('email') 26 | ) 27 | 28 | 29 | def downgrade(): 30 | op.drop_table('user') 31 | -------------------------------------------------------------------------------- /migrations/versions/37e782a55ca6_set_on_delete_cascad.py: -------------------------------------------------------------------------------- 1 | """Set ON DELETE CASCADE on Project* 2 | 3 | Revision ID: 37e782a55ca6 4 | Revises: 218e84b10a0e 5 | Create Date: 2013-12-23 16:41:27.462599 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '37e782a55ca6' 11 | down_revision = '218e84b10a0e' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.drop_constraint('project_repository_id_fkey', 'project') 18 | op.create_foreign_key('project_repository_id_fkey', 'project', 'repository', ['repository_id'], ['id'], ondelete='RESTRICT') 19 | 20 | op.drop_constraint('projectoption_project_id_fkey', 'projectoption') 21 | op.create_foreign_key('projectoption_project_id_fkey', 'projectoption', 'project', ['project_id'], ['id'], ondelete='CASCADE') 22 | 23 | 24 | def downgrade(): 25 | pass 26 | -------------------------------------------------------------------------------- /migrations/versions/524b3c27203b_remove_job_author_id.py: -------------------------------------------------------------------------------- 1 | """Remove Job.{author_id,repository_id,patch_id,revision_sha,message,target,cause,parent_id} 2 | 3 | Revision ID: 524b3c27203b 4 | Revises: 3042d0ca43bf 5 | Create Date: 2014-01-05 16:36:43.476520 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '524b3c27203b' 11 | down_revision = '3042d0ca43bf' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.drop_column('job', 'revision_sha') 18 | op.drop_column('job', 'patch_id') 19 | op.drop_column('job', 'author_id') 20 | op.drop_column('job', 'repository_id') 21 | op.drop_column('job', 'message') 22 | op.drop_column('job', 'target') 23 | op.drop_column('job', 'cause') 24 | op.drop_column('job', 'parent_id') 25 | 26 | 27 | def downgrade(): 28 | raise NotImplementedError 29 | -------------------------------------------------------------------------------- /static/vendor/react-bootstrap/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-bootstrap", 3 | "version": "0.29.3", 4 | "homepage": "http://react-bootstrap.github.io/", 5 | "author": "Stephen J. Collings ", 6 | "license": "MIT", 7 | "main": [ 8 | "react-bootstrap.js" 9 | ], 10 | "keywords": [ 11 | "react", 12 | "ecosystem-react", 13 | "react-component", 14 | "bootstrap" 15 | ], 16 | "ignore": [ 17 | "**/.*" 18 | ], 19 | "dependencies": { 20 | "react": ">=0.14.0" 21 | }, 22 | "_release": "0.29.3", 23 | "_resolution": { 24 | "type": "version", 25 | "tag": "v0.29.3", 26 | "commit": "9e09d0a906e396d754761a7343b6920b820df3aa" 27 | }, 28 | "_source": "https://github.com/react-bootstrap/react-bootstrap-bower.git", 29 | "_target": "~0.29", 30 | "_originalSource": "react-bootstrap" 31 | } -------------------------------------------------------------------------------- /changes/utils/locking.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | from functools import wraps 3 | from hashlib import md5 4 | 5 | from changes.ext.redis import UnableToGetLock 6 | from changes.config import redis 7 | 8 | 9 | def lock(func): 10 | @wraps(func) 11 | def wrapped(**kwargs): 12 | key = '{0}:{1}:{2}'.format( 13 | func.__module__, 14 | func.__name__, 15 | md5( 16 | '&'.join('{0}={1}'.format(k, repr(v)) 17 | for k, v in sorted(kwargs.iteritems())) 18 | ).hexdigest() 19 | ) 20 | try: 21 | with redis.lock(key, expire=300, nowait=True): 22 | return func(**kwargs) 23 | except UnableToGetLock: 24 | current_app.logger.warn('Unable to get lock for %s', key) 25 | raise 26 | 27 | return wrapped 28 | -------------------------------------------------------------------------------- /migrations/versions/4768ba7627ac_adjust_command_schema.py: -------------------------------------------------------------------------------- 1 | """Adjust Command schema 2 | 3 | Revision ID: 4768ba7627ac 4 | Revises: 3f6a69c14037 5 | Create Date: 2014-07-17 14:32:47.347947 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4768ba7627ac' 11 | down_revision = '3f6a69c14037' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('command', sa.Column('order', sa.Integer(), nullable=False, 19 | server_default='0')) 20 | op.alter_column('command', 'return_code', existing_type=sa.INTEGER(), 21 | nullable=True) 22 | 23 | 24 | def downgrade(): 25 | op.alter_column('command', 'return_code', existing_type=sa.INTEGER(), 26 | nullable=False) 27 | op.drop_column('command', 'order') 28 | -------------------------------------------------------------------------------- /migrations/versions/5026dbcee21d_test_build_id_job_id.py: -------------------------------------------------------------------------------- 1 | """Test*.build_id => job_id 2 | 3 | Revision ID: 5026dbcee21d 4 | Revises: 42e9d35a4098 5 | Create Date: 2013-12-25 23:50:12.762986 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '5026dbcee21d' 11 | down_revision = '42e9d35a4098' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.execute('ALTER TABLE test RENAME COLUMN build_id TO job_id') 18 | op.execute('ALTER TABLE testgroup RENAME COLUMN build_id TO job_id') 19 | op.execute('ALTER TABLE testsuite RENAME COLUMN build_id TO job_id') 20 | 21 | 22 | def downgrade(): 23 | op.execute('ALTER TABLE test RENAME COLUMN job_id TO build_id') 24 | op.execute('ALTER TABLE testgroup RENAME COLUMN job_id TO build_id') 25 | op.execute('ALTER TABLE testsuite RENAME COLUMN job_id TO build_id') 26 | -------------------------------------------------------------------------------- /static/vendor/underscore/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "underscore", 3 | "version": "1.8.3", 4 | "main": "underscore.js", 5 | "keywords": [ 6 | "util", 7 | "functional", 8 | "server", 9 | "client", 10 | "browser" 11 | ], 12 | "ignore": [ 13 | "docs", 14 | "test", 15 | "*.yml", 16 | "CNAME", 17 | "index.html", 18 | "favicon.ico", 19 | "CONTRIBUTING.md", 20 | ".*", 21 | "component.json", 22 | "package.json", 23 | "karma.*" 24 | ], 25 | "homepage": "https://github.com/jashkenas/underscore", 26 | "_release": "1.8.3", 27 | "_resolution": { 28 | "type": "version", 29 | "tag": "1.8.3", 30 | "commit": "e4743ab712b8ab42ad4ccb48b155034d02394e4d" 31 | }, 32 | "_source": "https://github.com/jashkenas/underscore.git", 33 | "_target": "~1.8.3", 34 | "_originalSource": "underscore" 35 | } -------------------------------------------------------------------------------- /migrations/versions/19910015c867_add_systemoption.py: -------------------------------------------------------------------------------- 1 | """Add SystemOption 2 | 3 | Revision ID: 19910015c867 4 | Revises: 2b7153fe25af 5 | Create Date: 2014-07-11 15:48:26.276042 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '19910015c867' 11 | down_revision = '2b7153fe25af' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.create_table( 19 | 'systemoption', 20 | sa.Column('id', sa.GUID(), nullable=False), 21 | sa.Column('name', sa.String(length=64), nullable=False), 22 | sa.Column('value', sa.Text(), nullable=False), 23 | sa.Column('date_created', sa.DateTime(), nullable=False), 24 | sa.PrimaryKeyConstraint('id'), 25 | sa.UniqueConstraint('name') 26 | ) 27 | 28 | 29 | def downgrade(): 30 | op.drop_table('systemoption') 31 | -------------------------------------------------------------------------------- /tests/changes/api/validators/test_datetime.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytest 4 | from changes.api.validators.datetime import ISODatetime 5 | 6 | 7 | def test_isodatetime_valid(): 8 | validator = ISODatetime() 9 | assert validator('2016-09-01T19:46:18.9Z') == datetime(year=2016, 10 | month=9, 11 | day=1, 12 | hour=19, 13 | minute=46, 14 | second=18, 15 | microsecond=900000) 16 | 17 | 18 | def test_isodatetime_invalid(): 19 | with pytest.raises(ValueError): 20 | ISODatetime()('invalid') 21 | -------------------------------------------------------------------------------- /changes/api/serializer/models/change.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.change import Change 3 | from changes.utils.http import build_web_uri 4 | 5 | 6 | @register(Change) 7 | class ChangeCrumbler(Crumbler): 8 | def crumble(self, instance, attrs): 9 | result = { 10 | 'id': instance.id.hex, 11 | 'name': instance.label, 12 | 'project': instance.project, 13 | 'author': instance.author, 14 | 'message': instance.message, 15 | 'link': build_web_uri('/changes/%s/' % (instance.id.hex,)), 16 | 'dateCreated': instance.date_created.isoformat(), 17 | 'dateModified': instance.date_modified.isoformat(), 18 | } 19 | if hasattr(instance, 'last_job'): 20 | result['lastBuild'] = instance.last_job 21 | return result 22 | -------------------------------------------------------------------------------- /changes/artifacts/dummylogfile.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .base import ArtifactHandler 4 | 5 | 6 | class DummyLogFileHandler(ArtifactHandler): 7 | """ 8 | Dummy Artifact handler for log files. 9 | We only fetch artifacts from Jenkins masters if (according to the registered 10 | ArtifactHandlers) we are interested in the contents. 11 | For projects using ArtifactStore this works out fine, but for others it means 12 | that they'll only have reliable access to the contents of artifacts that are processed. 13 | 14 | This handler exists to signal our interest in the contents of *.log files in those cases 15 | where they won't be available otherwise. 16 | """ 17 | FILENAMES = ('*.log',) 18 | 19 | def process(self, fp, artifact): 20 | # We don't need to do anything with the file contents. 21 | pass 22 | -------------------------------------------------------------------------------- /migrations/versions/11a7a7f2652f_add_itemstat.py: -------------------------------------------------------------------------------- 1 | """Add ItemStat 2 | 3 | Revision ID: 11a7a7f2652f 4 | Revises: 4276d58dd1e6 5 | Create Date: 2014-03-13 13:33:32.840399 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '11a7a7f2652f' 11 | down_revision = '4276d58dd1e6' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.create_table( 19 | 'itemstat', 20 | sa.Column('id', sa.GUID(), nullable=False), 21 | sa.Column('item_id', sa.GUID(), nullable=False), 22 | sa.Column('name', sa.String(length=64), nullable=False), 23 | sa.Column('value', sa.Integer(), nullable=False), 24 | sa.PrimaryKeyConstraint('id'), 25 | sa.UniqueConstraint('item_id', 'name', name='unq_itemstat_name') 26 | ) 27 | 28 | 29 | def downgrade(): 30 | op.drop_table('itemstat') 31 | -------------------------------------------------------------------------------- /webapp/pages/examples_page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Examples from 'es6!display/examples'; 4 | import { ChangesPage } from 'es6!display/page_chrome'; 5 | 6 | /* 7 | * Renders example uses of the reusable display tags in display/ 8 | */ 9 | var DisplayExamplesPage = React.createClass({ 10 | 11 | getInitialTitle: function() { 12 | return "Examples"; 13 | }, 14 | 15 | render: function() { 16 | var removeHref = URI(window.location.href) 17 | .addQuery('disable_custom', 1) 18 | .toString(); 19 | 20 | var removeLink = 21 | Render without any custom JS/CSS 22 | ; 23 | 24 | return 25 |
26 | {removeLink} 27 |
28 | {Examples.render()} 29 |
; 30 | } 31 | }); 32 | 33 | export default DisplayExamplesPage; 34 | -------------------------------------------------------------------------------- /tests/changes/api/test_job_artifact_index.py: -------------------------------------------------------------------------------- 1 | from changes.testutils import APITestCase 2 | 3 | 4 | class JobArtifactIndexTest(APITestCase): 5 | def test_simple(self): 6 | project = self.create_project() 7 | build = self.create_build(project) 8 | job = self.create_job(build) 9 | jobphase = self.create_jobphase(job) 10 | jobstep = self.create_jobstep(jobphase) 11 | artifact_1 = self.create_artifact(jobstep, 'junit.xml') 12 | artifact_2 = self.create_artifact(jobstep, 'coverage.xml') 13 | 14 | path = '/api/0/jobs/{0}/artifacts/'.format(job.id.hex) 15 | 16 | resp = self.client.get(path) 17 | assert resp.status_code == 200 18 | data = self.unserialize(resp) 19 | assert len(data) == 2 20 | assert data[0]['id'] == artifact_2.id.hex 21 | assert data[1]['id'] == artifact_1.id.hex 22 | -------------------------------------------------------------------------------- /changes/lib/project_lib.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import literal 2 | from changes.config import db 3 | from changes.models.option import ItemOption 4 | from changes.models.plan import Plan, PlanStatus 5 | from changes.models.project import Project # NOQA 6 | 7 | 8 | def contains_active_autogenerated_plan(project): 9 | """Returns true if there are any active autogenerated build plans for 10 | this project. 11 | """ 12 | # type: (Project)->bool 13 | contains = db.session.query(literal(True)).filter( 14 | ItemOption.query.join( 15 | Plan, Plan.id == ItemOption.item_id, 16 | ).filter( 17 | ItemOption.name == 'bazel.autogenerate', 18 | ItemOption.value == '1', 19 | Plan.project_id == project.id, 20 | Plan.status == PlanStatus.active, 21 | ).exists() 22 | ).scalar() 23 | return bool(contains) 24 | -------------------------------------------------------------------------------- /migrations/versions/18c045b75331_add_various_build_in.py: -------------------------------------------------------------------------------- 1 | """Add various Build indexes 2 | 3 | Revision ID: 18c045b75331 4 | Revises: d324e8fc580 5 | Create Date: 2013-11-04 17:14:33.455855 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '18c045b75331' 11 | down_revision = 'd324e8fc580' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_index('idx_build_project_id', 'build', ['project_id']) 18 | op.create_index('idx_build_repository_id', 'build', ['repository_id']) 19 | op.create_index('idx_build_author_id', 'build', ['author_id']) 20 | op.create_index('idx_build_patch_id', 'build', ['patch_id']) 21 | op.create_index('idx_build_change_id', 'build', ['change_id']) 22 | 23 | 24 | def downgrade(): 25 | ### commands auto generated by Alembic - please adjust! ### 26 | pass 27 | ### end Alembic commands ### 28 | -------------------------------------------------------------------------------- /changes/api/controller.py: -------------------------------------------------------------------------------- 1 | from flask.signals import got_request_exception 2 | from flask.ext.restful import Api, Resource 3 | 4 | 5 | class APIController(Api): 6 | def handle_error(self, e): 7 | """ 8 | Almost identical to Flask-Restful's handle_error, but fixes some minor 9 | issues. 10 | 11 | Specifically, this fixes exceptions so they get propagated correctly 12 | when ``propagate_exceptions`` is set. 13 | """ 14 | if not hasattr(e, 'code') and self.app.propagate_exceptions: 15 | got_request_exception.send(self.app, exception=e) 16 | raise 17 | 18 | return super(APIController, self).handle_error(e) 19 | 20 | 21 | class APICatchall(Resource): 22 | def get(self, path): 23 | return {'error': 'Not Found'}, 404 24 | 25 | post = get 26 | put = get 27 | delete = get 28 | patch = get 29 | -------------------------------------------------------------------------------- /changes/api/task_details.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from changes.api.base import APIView 4 | from changes.models.task import Task 5 | 6 | 7 | class TaskDetailsAPIView(APIView): 8 | def _collect_children(self, task): 9 | children = Task.query.filter( 10 | Task.parent_id == task.task_id, 11 | ) 12 | results = [] 13 | for child in children: 14 | child_data = self.serialize(child) 15 | child_data['children'] = self._collect_children(child) 16 | results.append(child_data) 17 | 18 | return results 19 | 20 | def get(self, task_id): 21 | task = Task.query.get(task_id) 22 | if task is None: 23 | return '', 404 24 | 25 | context = self.serialize(task) 26 | context['children'] = self._collect_children(task) 27 | 28 | return self.respond(context) 29 | -------------------------------------------------------------------------------- /migrations/versions/1b53d33197bf_add_build_cause_and_.py: -------------------------------------------------------------------------------- 1 | """Add Build.cause and Buld.parent 2 | 3 | Revision ID: 1b53d33197bf 4 | Revises: 2b8459f1e2d6 5 | Create Date: 2013-10-25 14:33:55.401260 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '1b53d33197bf' 11 | down_revision = '2b8459f1e2d6' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('build', sa.Column('cause', sa.Enum(), server_default='0', nullable=False)) 20 | op.add_column('build', sa.Column('parent', sa.GUID(), nullable=True)) 21 | ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('build', 'parent') 27 | op.drop_column('build', 'cause') 28 | ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/21c9439330f_add_cascade_to_filec.py: -------------------------------------------------------------------------------- 1 | """Add cascade to FileCoverage 2 | 3 | Revision ID: 21c9439330f 4 | Revises: 1f5caa34d9c2 5 | Create Date: 2014-04-01 15:29:26.765288 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '21c9439330f' 11 | down_revision = '1f5caa34d9c2' 12 | 13 | from alembic import op 14 | 15 | 16 | def upgrade(): 17 | op.create_index('idx_filecoverage_job_id', 'filecoverage', ['job_id']) 18 | op.create_index('idx_filecoverage_project_id', 'filecoverage', ['project_id']) 19 | 20 | op.drop_constraint('filecoverage_build_id_fkey', 'filecoverage') 21 | op.create_foreign_key('filecoverage_job_id_fkey', 'filecoverage', 'job', ['job_id'], ['id'], ondelete='CASCADE') 22 | 23 | op.create_foreign_key('filecoverage_project_id_fkey', 'filecoverage', 'project', ['project_id'], ['id'], ondelete='CASCADE') 24 | 25 | 26 | def downgrade(): 27 | pass 28 | -------------------------------------------------------------------------------- /changes/buildsteps/dummy.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from changes.buildsteps.base import BuildStep 4 | from changes.config import db 5 | from changes.constants import Status, Result 6 | 7 | 8 | class DummyBuildStep(BuildStep): 9 | def get_label(self): 10 | return 'do nothing' 11 | 12 | def execute(self, job): 13 | job.status = Status.finished 14 | job.result = Result.aborted 15 | db.session.add(job) 16 | 17 | def update(self, job): 18 | job.status = Status.finished 19 | job.result = Result.aborted 20 | db.session.add(job) 21 | 22 | def update_step(self, step): 23 | step.status = Status.finished 24 | step.result = Result.aborted 25 | db.session.add(step) 26 | 27 | def cancel_step(self, step): 28 | step.status = Status.finished 29 | step.result = Result.aborted 30 | db.session.add(step) 31 | -------------------------------------------------------------------------------- /migrations/versions/2d8acbec66df_add_build_target.py: -------------------------------------------------------------------------------- 1 | """Add Build.target 2 | 3 | Revision ID: 2d8acbec66df 4 | Revises: 5aad6f742b77 5 | Create Date: 2013-11-11 17:07:11.624071 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2d8acbec66df' 11 | down_revision = '5aad6f742b77' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('build', sa.Column('target', sa.String(length=128), nullable=True)) 20 | op.drop_column('build', u'parent_revision_sha') 21 | ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | ### commands auto generated by Alembic - please adjust! ### 26 | op.add_column('build', sa.Column(u'parent_revision_sha', sa.VARCHAR(length=40), nullable=True)) 27 | op.drop_column('build', 'target') 28 | ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /tests/changes/api/serializer/models/test_bazeltargetmessage.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from changes.api.serializer import serialize 4 | from changes.testutils.cases import TestCase 5 | 6 | 7 | class BazelTargetMessageTestCase(TestCase): 8 | 9 | def test_correct(self): 10 | project = self.create_project() 11 | build = self.create_build(project) 12 | job = self.create_job(build) 13 | target = self.create_target(job) 14 | message = self.create_target_message( 15 | target, 16 | text="Test message", 17 | date_created=datetime(2013, 9, 19, 22, 15, 22), 18 | ) 19 | 20 | result = serialize(message) 21 | assert result['id'] == message.id.hex 22 | assert result['target']['id'] == message.target_id.hex 23 | assert result['text'] == message.text 24 | assert result['dateCreated'] == '2013-09-19T22:15:22' 25 | -------------------------------------------------------------------------------- /changes/api/serializer/models/jobphase.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.jobphase import JobPhase 3 | 4 | 5 | @register(JobPhase) 6 | class JobPhaseCrumbler(Crumbler): 7 | def crumble(self, instance, attrs): 8 | return { 9 | 'id': instance.id.hex, 10 | 'name': instance.label, 11 | 'result': instance.result, 12 | 'status': instance.status, 13 | 'duration': instance.duration, 14 | 'dateCreated': instance.date_created, 15 | 'dateStarted': instance.date_started, 16 | 'dateFinished': instance.date_finished, 17 | } 18 | 19 | 20 | class JobPhaseWithStepsCrumbler(JobPhaseCrumbler): 21 | def crumble(self, instance, attrs): 22 | data = super(JobPhaseWithStepsCrumbler, self).crumble(instance, attrs) 23 | data['steps'] = list(instance.steps) 24 | return data 25 | -------------------------------------------------------------------------------- /changes/api/validators/author.py: -------------------------------------------------------------------------------- 1 | from changes.models.author import Author 2 | from changes.db.utils import get_or_create 3 | 4 | 5 | class AuthorValidator(object): 6 | def __call__(self, value): 7 | parsed = self.parse(value) 8 | if not parsed: 9 | raise ValueError(value) 10 | 11 | name, email = parsed 12 | author, _ = get_or_create(Author, where={ 13 | 'email': email, 14 | }, defaults={ 15 | 'name': name, 16 | }) 17 | return author 18 | 19 | def parse(self, value): 20 | import re 21 | match = re.match(r'^(.+) <([^>]+)>$', value) 22 | 23 | if not match: 24 | if '@' in value: 25 | name, email = value, value 26 | else: 27 | raise ValueError(value) 28 | else: 29 | name, email = match.group(1), match.group(2) 30 | return name, email 31 | -------------------------------------------------------------------------------- /changes/listeners/snapshot_build.py: -------------------------------------------------------------------------------- 1 | from changes.config import db 2 | from changes.constants import Cause, Result 3 | from changes.models.build import Build 4 | from changes.models.snapshot import SnapshotStatus 5 | from changes.utils.locking import lock 6 | 7 | 8 | @lock 9 | def build_finished_handler(build_id, **kwargs): 10 | build = Build.query.get(build_id) 11 | 12 | # only handle snapshot builds 13 | if build is None or build.cause != Cause.snapshot: 14 | return 15 | 16 | # If the snapshot build did not pass then we guarantee that 17 | # the snapshot is either invalidated or is failed. 18 | if build.result != Result.passed and build.snapshot is not None: 19 | if build.snapshot.status == SnapshotStatus.active: 20 | build.snapshot.status = SnapshotStatus.invalidated 21 | else: 22 | build.snapshot.status = SnapshotStatus.failed 23 | db.session.commit() 24 | -------------------------------------------------------------------------------- /migrations/versions/4d302aa44bc8_add_additional_revis.py: -------------------------------------------------------------------------------- 1 | """Add additional revision data 2 | 3 | Revision ID: 4d302aa44bc8 4 | Revises: 215db24a630a 5 | Create Date: 2013-11-26 16:20:59.454360 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '4d302aa44bc8' 11 | down_revision = '215db24a630a' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('revision', sa.Column('committer_id', sa.GUID(), nullable=True)) 20 | op.add_column('revision', sa.Column('date_committed', sa.DateTime(), nullable=True)) 21 | ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('revision', 'date_committed') 27 | op.drop_column('revision', 'committer_id') 28 | ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **NOTICE: THIS REPO IS NO LONGER UPDATED** 2 | 3 | Changes 4 | ------- 5 | 6 | Changes is a build coordinator and reporting solution written in Python. 7 | 8 | The project is primarily built on top of Jenkins, but efforts are underway to 9 | replace the underlying dependency. The current work-in-progress tooling exists 10 | under several additional repositories: 11 | 12 | - https://github.com/dropbox/changes-client 13 | - https://github.com/dropbox/changes-mesos-framework 14 | 15 | .. note:: **Changes is under extremely active and rapid development, and you probably shouldn't use it unless you like broken software.** 16 | 17 | 18 | Screenshot 19 | ========== 20 | 21 | .. image:: https://github.com/dropbox/changes/raw/master/docs/images/example.png 22 | 23 | Getting Started 24 | =============== 25 | 26 | While still fairly rough, you'll want to refer to our `documentation `_. 27 | -------------------------------------------------------------------------------- /tests/changes/api/test_build_target_message_index.py: -------------------------------------------------------------------------------- 1 | from changes.testutils import APITestCase 2 | 3 | 4 | class BuildTargetMessageIndexTestCase(APITestCase): 5 | path = '/api/0/builds/{build_id}/targets/{target_id}/messages/' 6 | 7 | def test_correct(self): 8 | build = self.create_build(self.create_project()) 9 | job = self.create_job(build) 10 | target = self.create_target(job) 11 | message1 = self.create_target_message(target, text="Test message") 12 | message2 = self.create_target_message(target, text="Test message") 13 | message3 = self.create_target_message(target, text="Test message") 14 | 15 | resp = self.client.get(self.path.format(build_id=build.id.hex, target_id=target.id.hex)) 16 | assert resp.status_code == 200 17 | data = self.unserialize(resp) 18 | 19 | assert [m['id'] for m in data] == [message1.id.hex, message2.id.hex, message3.id.hex] 20 | -------------------------------------------------------------------------------- /changes/api/build_test_index_counts.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import contains_eager 2 | 3 | from changes.api.base import APIView 4 | from changes.constants import Result 5 | from changes.models.build import Build 6 | from changes.models.job import Job 7 | from changes.models.test import TestCase 8 | 9 | 10 | class BuildTestIndexCountsAPIView(APIView): 11 | 12 | def get(self, build_id): 13 | build = Build.query.get(build_id) 14 | if build is None: 15 | return '', 404 16 | 17 | test_list = TestCase.query.options( 18 | contains_eager('job') 19 | ).join( 20 | Job, TestCase.job_id == Job.id, 21 | ).filter( 22 | Job.build_id == build.id, 23 | ) 24 | 25 | count_dict = {result.name: 0 for result in Result} 26 | 27 | for test in test_list: 28 | count_dict[test.result.name] += 1 29 | 30 | return self.respond(count_dict) 31 | -------------------------------------------------------------------------------- /changes/api/node_job_index.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from sqlalchemy.orm import joinedload 4 | 5 | from changes.api.base import APIView 6 | from changes.api.serializer.models.job import JobWithBuildCrumbler 7 | from changes.models.job import Job 8 | from changes.models.jobstep import JobStep 9 | from changes.models.node import Node 10 | 11 | 12 | class NodeJobIndexAPIView(APIView): 13 | def get(self, node_id): 14 | node = Node.query.get(node_id) 15 | if node is None: 16 | return '', 404 17 | 18 | jobs = Job.query.join( 19 | JobStep, JobStep.job_id == Job.id, 20 | ).options( 21 | joinedload(Job.build, innerjoin=True), 22 | ).filter( 23 | JobStep.node_id == node.id, 24 | ).order_by(Job.date_created.desc()) 25 | 26 | return self.paginate(jobs, serializers={ 27 | Job: JobWithBuildCrumbler(), 28 | }) 29 | -------------------------------------------------------------------------------- /migrations/versions/2b1b01d74175_added_adminmessage_table.py: -------------------------------------------------------------------------------- 1 | """Added adminmessage table 2 | 3 | Revision ID: 2b1b01d74175 4 | Revises: 1cb3db431e29 5 | Create Date: 2015-04-15 22:55:56.097518 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2b1b01d74175' 11 | down_revision = '1cb3db431e29' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.create_table('adminmessage', 19 | sa.Column('id', sa.GUID(), nullable=False), 20 | sa.Column('user_id', sa.GUID(), nullable=True), 21 | sa.Column('date_created', sa.DateTime(), nullable=True), 22 | sa.Column('message', sa.Text(), nullable=True), 23 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), 24 | sa.PrimaryKeyConstraint('id')) 25 | 26 | 27 | def downgrade(): 28 | op.drop_table('adminmessage') 29 | -------------------------------------------------------------------------------- /migrations/versions/32274e97552_build_parent_build_p.py: -------------------------------------------------------------------------------- 1 | """Build.parent -> Build.parent_id 2 | 3 | Revision ID: 32274e97552 4 | Revises: 1b53d33197bf 5 | Create Date: 2013-10-25 15:41:21.120737 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '32274e97552' 11 | down_revision = '1b53d33197bf' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | from sqlalchemy.dialects import postgresql 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('build', sa.Column('parent_id', sa.GUID(), nullable=True)) 20 | op.drop_column('build', u'parent') 21 | ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | ### commands auto generated by Alembic - please adjust! ### 26 | op.add_column('build', sa.Column(u'parent', postgresql.UUID(), nullable=True)) 27 | op.drop_column('build', 'parent_id') 28 | ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/346c011ca77a_optional_parent_revi.py: -------------------------------------------------------------------------------- 1 | """Optional parent_revision_sha 2 | 3 | Revision ID: 346c011ca77a 4 | Revises: 2d8acbec66df 5 | Create Date: 2013-11-11 17:08:40.087545 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '346c011ca77a' 11 | down_revision = '2d8acbec66df' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.alter_column('patch', 'parent_revision_sha', 20 | existing_type=sa.VARCHAR(length=40), 21 | nullable=True) 22 | ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | ### commands auto generated by Alembic - please adjust! ### 27 | op.alter_column('patch', 'parent_revision_sha', 28 | existing_type=sa.VARCHAR(length=40), 29 | nullable=False) 30 | ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /tests/changes/api/serializer/models/test_adminmessage.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from uuid import UUID 3 | 4 | from changes.api.serializer import serialize 5 | from changes.models.adminmessage import AdminMessage 6 | from changes.testutils import TestCase 7 | 8 | 9 | class AdminMessageTest(TestCase): 10 | def test_simple(self): 11 | message = AdminMessage( 12 | id=UUID(hex='33846695b2774b29a71795a009e8168a'), 13 | message='Foo bar', 14 | user=self.create_user( 15 | email='foo@example.com', 16 | ), 17 | date_created=datetime(2013, 9, 19, 22, 15, 22), 18 | ) 19 | result = serialize(message) 20 | assert result['id'] == '33846695b2774b29a71795a009e8168a' 21 | assert result['user']['id'] == message.user.id.hex 22 | assert result['message'] == 'Foo bar' 23 | assert result['dateCreated'] == '2013-09-19T22:15:22' 24 | -------------------------------------------------------------------------------- /changes/buildfailures/testfailure.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from jinja2 import Markup 4 | 5 | from changes.buildfailures.base import BuildFailure 6 | from changes.utils.http import build_web_uri 7 | 8 | 9 | class TestFailure(BuildFailure): 10 | def get_html_label(self, build): 11 | link = build_web_uri('/projects/{0}/builds/{1}/tests/?result=failed'.format(build.project.slug, build.id.hex)) 12 | 13 | try: 14 | test_failures = ( 15 | s.value for s in build.stats if s.name == 'test_failures' 16 | ).next() 17 | except StopIteration: 18 | return Markup('There were an unknown number of test failures.'.format( 19 | link=link, 20 | )) 21 | 22 | return Markup('There were {count} failing tests.'.format( 23 | link=link, 24 | count=test_failures, 25 | )) 26 | -------------------------------------------------------------------------------- /tests/changes/api/serializer/models/test_change.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from uuid import UUID 3 | 4 | from changes.api.serializer import serialize 5 | from changes.models.change import Change 6 | from changes.models.project import Project 7 | 8 | 9 | def test_simple(): 10 | change = Change( 11 | id=UUID(hex='33846695b2774b29a71795a009e8168a'), 12 | label='Hello world', 13 | project=Project(slug='test', name='test'), 14 | date_created=datetime(2013, 9, 19, 22, 15, 22), 15 | date_modified=datetime(2013, 9, 19, 22, 15, 23), 16 | ) 17 | result = serialize(change) 18 | assert result['name'] == 'Hello world' 19 | assert result['link'] == 'http://example.com/changes/33846695b2774b29a71795a009e8168a/' 20 | assert result['id'] == '33846695b2774b29a71795a009e8168a' 21 | assert result['dateCreated'] == '2013-09-19T22:15:22' 22 | assert result['dateModified'] == '2013-09-19T22:15:23' 23 | -------------------------------------------------------------------------------- /tests/changes/api/serializer/models/test_logchunk.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from uuid import UUID 3 | 4 | from changes.api.serializer import serialize 5 | from changes.models.log import LogSource, LogChunk 6 | 7 | 8 | def test_simple(): 9 | logchunk = LogChunk( 10 | id=UUID(hex='33846695b2774b29a71795a009e8168a'), 11 | source_id=UUID(hex='0b61b8a47ec844918d372d5741187b1c'), 12 | source=LogSource(id=UUID(hex='0b61b8a47ec844918d372d5741187b1c')), 13 | offset=10, 14 | size=7, 15 | text='\x1b[0;36mnotice: foo bar', 16 | date_created=datetime(2013, 9, 19, 22, 15, 22), 17 | ) 18 | result = serialize(logchunk) 19 | assert result['id'] == '33846695b2774b29a71795a009e8168a' 20 | assert result['source']['id'] == '0b61b8a47ec844918d372d5741187b1c' 21 | assert result['text'] == '\x1b[0;36mnotice: foo bar' 22 | assert result['size'] == 7 23 | assert result['offset'] == 10 24 | -------------------------------------------------------------------------------- /changes/api/serializer/models/command.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.command import Command 3 | 4 | 5 | @register(Command) 6 | class CommandCrumbler(Crumbler): 7 | def crumble(self, instance, attrs): 8 | return { 9 | 'id': instance.id.hex, 10 | 'name': instance.label, 11 | 'status': instance.status, 12 | 'script': instance.script, 13 | 'returnCode': instance.return_code, 14 | 'env': dict(instance.env or {}), 15 | 'cwd': instance.cwd, 16 | 'type': instance.type, 17 | 'captureOutput': instance.type.is_collector(), 18 | 'artifacts': instance.artifacts or [], 19 | 'duration': instance.duration, 20 | 'dateCreated': instance.date_created, 21 | 'dateStarted': instance.date_started, 22 | 'dateFinished': instance.date_finished, 23 | } 24 | -------------------------------------------------------------------------------- /changes/api/snapshotimage_details.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from flask.ext.restful import reqparse 4 | 5 | from changes.api.base import APIView 6 | from changes.models.snapshot import SnapshotImage, SnapshotStatus 7 | 8 | 9 | class SnapshotImageDetailsAPIView(APIView): 10 | parser = reqparse.RequestParser() 11 | parser.add_argument('status', choices=SnapshotStatus._member_names_) 12 | 13 | def get(self, image_id): 14 | image = SnapshotImage.query.get(image_id) 15 | if image is None: 16 | return '', 404 17 | 18 | return self.respond(image) 19 | 20 | def post(self, image_id): 21 | image = SnapshotImage.query.get(image_id) 22 | if image is None: 23 | return '', 404 24 | 25 | args = self.parser.parse_args() 26 | 27 | if args.status: 28 | image.change_status(SnapshotStatus[args.status]) 29 | 30 | return self.respond(image) 31 | -------------------------------------------------------------------------------- /migrations/versions/25d63e09ea3b_add_build_job_number.py: -------------------------------------------------------------------------------- 1 | """Add Build/Job numbers 2 | 3 | Revision ID: 25d63e09ea3b 4 | Revises: 1ad857c78a0d 5 | Create Date: 2013-12-26 02:36:58.663362 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '25d63e09ea3b' 11 | down_revision = '1ad857c78a0d' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('build', sa.Column('number', sa.Integer(), nullable=True)) 19 | op.add_column('job', sa.Column('number', sa.Integer(), nullable=True)) 20 | op.create_unique_constraint('unq_build_number', 'build', ['project_id', 'number']) 21 | op.create_unique_constraint('unq_job_number', 'job', ['build_id', 'number']) 22 | 23 | 24 | def downgrade(): 25 | op.drop_column('build', 'number') 26 | op.drop_column('job', 'number') 27 | op.drop_constraint('unq_build_number', 'build') 28 | op.drop_constraint('unq_job_number', 'job') 29 | -------------------------------------------------------------------------------- /changes/debug/mail/build_result.py: -------------------------------------------------------------------------------- 1 | from flask import render_template 2 | from flask.views import MethodView 3 | 4 | from changes.models.build import Build 5 | from changes.listeners.mail import MailNotificationHandler 6 | 7 | 8 | class BuildResultMailView(MethodView): 9 | def get(self, build_id): 10 | build = Build.query.get(build_id) 11 | assert build, 'There is no build for {}'.format(build_id) 12 | 13 | builds = list( 14 | Build.query.filter(Build.collection_id == build.collection_id)) 15 | notification_handler = MailNotificationHandler() 16 | context = notification_handler.get_collection_context(builds) 17 | msg = notification_handler.get_msg(context) 18 | return render_template( 19 | 'debug/email.html', 20 | recipients=msg.recipients, 21 | subject=msg.subject, 22 | text_content=msg.body, 23 | html_content=msg.html, 24 | ) 25 | -------------------------------------------------------------------------------- /changes/buildsteps/lxc.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from changes.buildsteps.default import DefaultBuildStep 4 | 5 | 6 | class LXCBuildStep(DefaultBuildStep): 7 | """ 8 | Similar to the default build step, except that it runs the client using 9 | the LXC adapter. 10 | """ 11 | def can_snapshot(self): 12 | return True 13 | 14 | def get_label(self): 15 | return 'Build via Changes Client (LXC)' 16 | 17 | def get_client_adapter(self): 18 | return 'lxc' 19 | 20 | @classmethod 21 | def custom_bin_path(cls): 22 | # This is where we mount custom binaries in the container 23 | return '/var/changes/input/' 24 | 25 | def get_allocation_params(self, jobstep): 26 | params = super(LXCBuildStep, self).get_allocation_params(jobstep) 27 | params['memory'] = str(self.resources['mem']) 28 | params['cpus'] = str(self.resources['cpus']) 29 | return params 30 | -------------------------------------------------------------------------------- /tests/changes/utils/test_http.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import uuid 3 | import mock 4 | 5 | from changes.utils.http import build_patch_uri 6 | 7 | 8 | class TestBuildPatchUri(unittest.TestCase): 9 | def test_internal(self): 10 | patch_id = uuid.UUID('ee62b37b-bc3f-4efe-83bc-f75152d60405') 11 | app = mock.Mock(config={'INTERNAL_BASE_URI': 'https://base_uri/'}) 12 | uri = build_patch_uri(patch_id, app) 13 | assert uri == 'https://base_uri/api/0/patches/{0}/?raw=1'.format( 14 | patch_id.hex) 15 | 16 | def test_use_patch(self): 17 | patch_id = uuid.UUID('ee62b37b-bc3f-4efe-83bc-f75152d60405') 18 | app = mock.Mock(config={ 19 | 'INTERNAL_BASE_URI': 'https://base_uri/', 20 | 'PATCH_BASE_URI': 'https://patch_uri/' 21 | }) 22 | uri = build_patch_uri(patch_id, app) 23 | assert uri == 'https://patch_uri/api/0/patches/{0}/?raw=1'.format( 24 | patch_id.hex) 25 | -------------------------------------------------------------------------------- /migrations/versions/215db24a630a_add_repository_last_.py: -------------------------------------------------------------------------------- 1 | """Add Repository.last_update, last_update_attempt 2 | 3 | Revision ID: 215db24a630a 4 | Revises: 208224023555 5 | Create Date: 2013-11-25 15:41:16.798396 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '215db24a630a' 11 | down_revision = '208224023555' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('repository', sa.Column('last_update', sa.DateTime(), nullable=True)) 20 | op.add_column('repository', sa.Column('last_update_attempt', sa.DateTime(), nullable=True)) 21 | ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('repository', 'last_update_attempt') 27 | op.drop_column('repository', 'last_update') 28 | ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /changes/api/serializer/models/task.py: -------------------------------------------------------------------------------- 1 | from changes.api.serializer import Crumbler, register 2 | from changes.models.task import Task 3 | 4 | 5 | @register(Task) 6 | class TaskCrumbler(Crumbler): 7 | def crumble(self, instance, attrs): 8 | if instance.data: 9 | args = instance.data.get('kwargs') or {} 10 | else: 11 | args = {} 12 | 13 | return { 14 | 'id': instance.id.hex, 15 | 'objectID': instance.task_id, 16 | 'parentObjectID': instance.parent_id, 17 | 'name': instance.task_name, 18 | 'args': args, 19 | 'attempts': instance.num_retries + 1, 20 | 'status': instance.status, 21 | 'result': instance.result, 22 | 'dateCreated': instance.date_created, 23 | 'dateStarted': instance.date_started, 24 | 'dateFinished': instance.date_finished, 25 | 'dateModified': instance.date_modified, 26 | } 27 | --------------------------------------------------------------------------------