summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlfredo Sumaran <alfredo@gitlab.com>2016-04-21 16:03:33 -0500
committerAlfredo Sumaran <alfredo@gitlab.com>2016-04-21 16:03:33 -0500
commitc9fe9f59d27bacb60c264678cce3cbe45d514a69 (patch)
tree0c7c497a5fbb96264d0c19a08aee17ad31f7fb05
parent80ae3790b456b12314243c75632ed6024ac16424 (diff)
parentc22c4048ce1a4233c0a6f9a9cd59a53efe49da53 (diff)
downloadgitlab-ce-c9fe9f59d27bacb60c264678cce3cbe45d514a69.tar.gz
Merge remote-tracking branch 'origin/master' into issue_3799
-rw-r--r--.gitlab-ci.yml5
-rw-r--r--.rubocop.yml2
-rw-r--r--CHANGELOG44
-rw-r--r--CONTRIBUTING.md1
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock3
-rw-r--r--PROCESS.md19
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/api.js.coffee11
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js.coffee30
-rw-r--r--app/assets/javascripts/blob/edit_blob.js.coffee67
-rw-r--r--app/assets/javascripts/blob/new_blob.js.coffee20
-rw-r--r--app/assets/javascripts/commits.js.coffee2
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee3
-rw-r--r--app/assets/javascripts/due_date_select.js.coffee64
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee18
-rw-r--r--app/assets/javascripts/issuable.js.coffee84
-rw-r--r--app/assets/javascripts/issuable_context.js.coffee38
-rw-r--r--app/assets/javascripts/issues.js.coffee51
-rw-r--r--app/assets/javascripts/labels_select.js.coffee141
-rw-r--r--app/assets/javascripts/lib/animate.js.coffee34
-rw-r--r--app/assets/javascripts/lib/url_utility.js.coffee16
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee12
-rw-r--r--app/assets/javascripts/merge_requests.js.coffee35
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee6
-rw-r--r--app/assets/javascripts/todos.js.coffee43
-rw-r--r--app/assets/javascripts/users_select.js.coffee2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss132
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss9
-rw-r--r--app/assets/stylesheets/framework/variables.scss5
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss5
-rw-r--r--app/assets/stylesheets/pages/editor.scss9
-rw-r--r--app/assets/stylesheets/pages/help.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss9
-rw-r--r--app/assets/stylesheets/pages/note_form.scss13
-rw-r--r--app/assets/stylesheets/pages/notes.scss24
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/admin/hooks_controller.rb2
-rw-r--r--app/controllers/projects/builds_controller.rb10
-rw-r--r--app/controllers/projects/commit_controller.rb45
-rw-r--r--app/controllers/projects/group_links_controller.rb10
-rw-r--r--app/controllers/projects/issues_controller.rb12
-rw-r--r--app/controllers/projects/merge_requests_controller.rb6
-rw-r--r--app/controllers/projects/project_members_controller.rb2
-rw-r--r--app/controllers/projects/services_controller.rb2
-rw-r--r--app/controllers/projects/wikis_controller.rb17
-rw-r--r--app/finders/issuable_finder.rb44
-rw-r--r--app/helpers/application_helper.rb9
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/blob_helper.rb11
-rw-r--r--app/helpers/ci_status_helper.rb15
-rw-r--r--app/helpers/commits_helper.rb31
-rw-r--r--app/helpers/gitlab_routing_helper.rb4
-rw-r--r--app/helpers/import_helper.rb18
-rw-r--r--app/helpers/issuables_helper.rb19
-rw-r--r--app/helpers/issues_helper.rb14
-rw-r--r--app/helpers/page_layout_helper.rb8
-rw-r--r--app/helpers/projects_helper.rb63
-rw-r--r--app/helpers/selects_helper.rb25
-rw-r--r--app/helpers/sorting_helper.rb18
-rw-r--r--app/helpers/tab_helper.rb8
-rw-r--r--app/helpers/tree_helper.rb2
-rw-r--r--app/mailers/emails/merge_requests.rb2
-rw-r--r--app/mailers/emails/notes.rb2
-rw-r--r--app/mailers/repository_check_mailer.rb2
-rw-r--r--app/models/ci/build.rb45
-rw-r--r--app/models/ci/commit.rb142
-rw-r--r--app/models/commit.rb19
-rw-r--r--app/models/commit_status.rb56
-rw-r--r--app/models/concerns/issuable.rb9
-rw-r--r--app/models/concerns/statuseable.rb81
-rw-r--r--app/models/group.rb1
-rw-r--r--app/models/hooks/project_hook.rb3
-rw-r--r--app/models/hooks/system_hook.rb3
-rw-r--r--app/models/hooks/web_hook.rb3
-rw-r--r--app/models/issue.rb46
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/merge_request.rb9
-rw-r--r--app/models/merge_request_diff.rb2
-rw-r--r--app/models/note.rb1
-rw-r--r--app/models/project.rb30
-rw-r--r--app/models/project_import_data.rb1
-rw-r--r--app/models/project_services/hipchat_service.rb4
-rw-r--r--app/models/project_services/slack_service.rb5
-rw-r--r--app/models/project_services/slack_service/merge_message.rb2
-rw-r--r--app/models/project_services/slack_service/note_message.rb2
-rw-r--r--app/models/project_services/slack_service/wiki_page_message.rb53
-rw-r--r--app/models/repository.rb86
-rw-r--r--app/models/service.rb4
-rw-r--r--app/models/user.rb1
-rw-r--r--app/models/wiki_page.rb4
-rw-r--r--app/services/ci/create_builds_service.rb27
-rw-r--r--app/services/ci/create_trigger_request_service.rb4
-rw-r--r--app/services/ci/image_for_build_service.rb11
-rw-r--r--app/services/commits/change_service.rb47
-rw-r--r--app/services/commits/cherry_pick_service.rb19
-rw-r--r--app/services/commits/revert_service.rb46
-rw-r--r--app/services/create_commit_builds_service.rb10
-rw-r--r--app/services/git_push_service.rb6
-rw-r--r--app/services/git_tag_push_service.rb29
-rw-r--r--app/services/system_hooks_service.rb12
-rw-r--r--app/services/wiki_pages/base_service.rb27
-rw-r--r--app/services/wiki_pages/create_service.rb13
-rw-r--r--app/services/wiki_pages/update_service.rb11
-rw-r--r--app/views/admin/application_settings/_form.html.haml17
-rw-r--r--app/views/admin/hooks/index.html.haml36
-rw-r--r--app/views/admin/users/_form.html.haml6
-rw-r--r--app/views/dashboard/todos/index.html.haml1
-rw-r--r--app/views/doorkeeper/applications/index.html.haml1
-rw-r--r--app/views/import/github/status.html.haml4
-rw-r--r--app/views/layouts/_page.html.haml4
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml3
-rw-r--r--app/views/layouts/nav/_profile.html.haml14
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/layouts/profile.html.haml3
-rw-r--r--app/views/notify/closed_merge_request_email.html.haml2
-rw-r--r--app/views/notify/closed_merge_request_email.text.haml2
-rw-r--r--app/views/notify/merge_request_status_email.html.haml2
-rw-r--r--app/views/notify/merge_request_status_email.text.haml2
-rw-r--r--app/views/notify/merged_merge_request_email.html.haml2
-rw-r--r--app/views/notify/merged_merge_request_email.text.haml2
-rw-r--r--app/views/notify/new_merge_request_email.text.erb2
-rw-r--r--app/views/notify/note_merge_request_email.text.erb2
-rw-r--r--app/views/profiles/accounts/show.html.haml1
-rw-r--r--app/views/profiles/audit_log.html.haml1
-rw-r--r--app/views/profiles/emails/index.html.haml1
-rw-r--r--app/views/profiles/keys/index.html.haml1
-rw-r--r--app/views/profiles/notifications/show.html.haml1
-rw-r--r--app/views/profiles/passwords/edit.html.haml1
-rw-r--r--app/views/profiles/preferences/show.html.haml1
-rw-r--r--app/views/projects/_activity.html.haml5
-rw-r--r--app/views/projects/_builds_settings.html.haml3
-rw-r--r--app/views/projects/_last_commit.html.haml9
-rw-r--r--app/views/projects/_readme.html.haml4
-rw-r--r--app/views/projects/blob/_editor.html.haml6
-rw-r--r--app/views/projects/blob/diff.html.haml8
-rw-r--r--app/views/projects/blob/new.html.haml2
-rw-r--r--app/views/projects/builds/index.html.haml2
-rw-r--r--app/views/projects/builds/show.html.haml9
-rw-r--r--app/views/projects/ci/builds/_build.html.haml13
-rw-r--r--app/views/projects/commit/_builds.html.haml69
-rw-r--r--app/views/projects/commit/_change.html.haml (renamed from app/views/projects/commit/_revert.html.haml)20
-rw-r--r--app/views/projects/commit/_ci_commit.html.haml69
-rw-r--r--app/views/projects/commit/_commit_box.html.haml9
-rw-r--r--app/views/projects/commit/show.html.haml5
-rw-r--r--app/views/projects/commits/_commit.html.haml9
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/empty.html.haml6
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml13
-rw-r--r--app/views/projects/hooks/index.html.haml11
-rw-r--r--app/views/projects/issues/_issue.html.haml5
-rw-r--r--app/views/projects/issues/_related_branches.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml6
-rw-r--r--app/views/projects/merge_requests/edit.html.haml4
-rw-r--r--app/views/projects/merge_requests/invalid.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_builds.html.haml3
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/_merged.html.haml5
-rw-r--r--app/views/projects/merge_requests/widget/_merged_buttons.haml11
-rw-r--r--app/views/projects/notes/_note.html.haml4
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml7
-rw-r--r--app/views/projects/show.html.haml12
-rw-r--r--app/views/repository_check_mailer/notify.html.haml3
-rw-r--r--app/views/repository_check_mailer/notify.text.haml3
-rw-r--r--app/views/search/results/_merge_request.html.haml2
-rw-r--r--app/views/shared/_label_row.html.haml2
-rw-r--r--app/views/shared/_labels_row.html.haml3
-rw-r--r--app/views/shared/_service_settings.html.haml8
-rw-r--r--app/views/shared/_sort_dropdown.html.haml5
-rw-r--r--app/views/shared/issuable/_filter.html.haml8
-rw-r--r--app/views/shared/issuable/_form.html.haml20
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml44
-rw-r--r--app/views/shared/issuable/_label_page_create.html.haml17
-rw-r--r--app/views/shared/issuable/_label_page_default.html.haml20
-rw-r--r--app/views/shared/issuable/_nav.html.haml10
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml61
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml7
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--app/workers/post_receive.rb4
-rw-r--r--app/workers/repository_check/single_repository_worker.rb6
-rw-r--r--config/initializers/metrics.rb4
-rw-r--r--config/routes.rb2
-rw-r--r--db/fixtures/development/14_builds.rb2
-rw-r--r--db/migrate/20160226114608_add_trigram_indexes_for_searching.rb11
-rw-r--r--db/migrate/20160227120001_add_event_field_for_web_hook.rb5
-rw-r--r--db/migrate/20160227120047_add_event_to_services.rb5
-rw-r--r--db/migrate/20160310124959_add_due_date_to_issues.rb6
-rw-r--r--db/migrate/20160412173416_add_fields_to_ci_commit.rb8
-rw-r--r--db/migrate/20160412173417_update_ci_commit.rb35
-rw-r--r--db/migrate/20160412173418_add_ci_commit_indexes.rb19
-rw-r--r--db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb5
-rw-r--r--db/migrate/20160419120017_add_metrics_packet_size.rb5
-rw-r--r--db/schema.rb51
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/licenses.md147
-rw-r--r--doc/ci/api/builds.md108
-rw-r--r--doc/ci/runners/README.md6
-rw-r--r--doc/ci/yaml/README.md33
-rw-r--r--doc/downgrade_ee_to_ce/README.md82
-rw-r--r--doc/install/requirements.md20
-rw-r--r--doc/intro/README.md1
-rw-r--r--doc/system_hooks/system_hooks.md113
-rw-r--r--doc/update/README.md93
-rw-r--r--doc/web_hooks/web_hooks.md4
-rw-r--r--doc/workflow/README.md1
-rw-r--r--doc/workflow/cherry_pick_changes.md52
-rw-r--r--doc/workflow/img/cherry_pick_changes_commit.pngbin0 -> 353067 bytes
-rw-r--r--doc/workflow/img/cherry_pick_changes_commit_modal.pngbin0 -> 312659 bytes
-rw-r--r--doc/workflow/img/cherry_pick_changes_mr.pngbin0 -> 252085 bytes
-rw-r--r--doc/workflow/img/cherry_pick_changes_mr_modal.pngbin0 -> 225569 bytes
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md2
-rw-r--r--features/project/issues/filter_labels.feature1
-rw-r--r--features/project/source/browse_files.feature13
-rw-r--r--features/steps/dashboard/todos.rb10
-rw-r--r--features/steps/profile/active_tab.rb4
-rw-r--r--features/steps/project/commits/user_lookup.rb3
-rw-r--r--features/steps/project/forked_merge_requests.rb2
-rw-r--r--features/steps/project/issues/filter_labels.rb4
-rw-r--r--features/steps/project/merge_requests.rb2
-rw-r--r--features/steps/project/source/browse_files.rb4
-rw-r--r--features/steps/shared/builds.rb6
-rw-r--r--features/steps/shared/project.rb2
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/commit_statuses.rb21
-rw-r--r--lib/api/entities.rb12
-rw-r--r--lib/api/internal.rb2
-rw-r--r--lib/api/licenses.rb58
-rw-r--r--lib/api/tags.rb2
-rw-r--r--lib/banzai/filter/label_reference_filter.rb36
-rw-r--r--lib/ci/api/api.rb2
-rw-r--r--lib/ci/api/builds.rb33
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb75
-rw-r--r--lib/ci/status.rb19
-rw-r--r--lib/file_size_validator.rb8
-rw-r--r--lib/gitlab/github_import/importer.rb13
-rw-r--r--lib/gitlab/github_import/issue_formatter.rb7
-rw-r--r--lib/gitlab/github_import/milestone_formatter.rb48
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb7
-rw-r--r--lib/gitlab/metrics.rb16
-rw-r--r--lib/gitlab/push_data_builder.rb3
-rw-r--r--lib/gitlab/sidekiq_middleware/memory_killer.rb4
-rw-r--r--lib/gitlab/url_builder.rb6
-rwxr-xr-xscripts/prepare_build.sh2
-rw-r--r--spec/controllers/commit_controller_spec.rb51
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb50
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb16
-rw-r--r--spec/factories/project_wikis.rb7
-rw-r--r--spec/factories/wiki_pages.rb9
-rw-r--r--spec/features/builds_spec.rb30
-rw-r--r--spec/features/issues/filter_by_labels_spec.rb167
-rw-r--r--spec/features/issues/filter_issues_spec.rb9
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb79
-rw-r--r--spec/features/issues/update_issues_spec.rb2
-rw-r--r--spec/features/issues_spec.rb98
-rw-r--r--spec/features/merge_requests/cherry_pick_spec.rb44
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb6
-rw-r--r--spec/features/project/commits/cherry_pick_spec.rb67
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb61
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb39
-rw-r--r--spec/features/runners_spec.rb16
-rw-r--r--spec/features/security/project/internal_access_spec.rb10
-rw-r--r--spec/features/security/project/private_access_spec.rb6
-rw-r--r--spec/features/security/project/public_access_spec.rb12
-rw-r--r--spec/features/todos/todos_spec.rb79
-rw-r--r--spec/finders/issues_finder_spec.rb16
-rw-r--r--spec/helpers/ci_status_helper_spec.rb6
-rw-r--r--spec/helpers/commits_helper_spec.rb29
-rw-r--r--spec/helpers/import_helper_spec.rb25
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb42
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb102
-rw-r--r--spec/lib/gitlab/badge/build_spec.rb6
-rw-r--r--spec/lib/gitlab/github_import/issue_formatter_spec.rb38
-rw-r--r--spec/lib/gitlab/github_import/milestone_formatter_spec.rb82
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb55
-rw-r--r--spec/lib/gitlab/metrics_spec.rb24
-rw-r--r--spec/lib/gitlab/push_data_builder_spec.rb15
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb9
-rw-r--r--spec/mailers/notify_spec.rb12
-rw-r--r--spec/mailers/repository_check_mailer_spec.rb2
-rw-r--r--spec/models/ci/commit_spec.rb213
-rw-r--r--spec/models/commit_spec.rb8
-rw-r--r--spec/models/commit_status_spec.rb64
-rw-r--r--spec/models/concerns/issuable_spec.rb30
-rw-r--r--spec/models/concerns/statuseable_spec.rb (renamed from spec/lib/ci/status_spec.rb)15
-rw-r--r--spec/models/issue_spec.rb41
-rw-r--r--spec/models/merge_request_spec.rb4
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb4
-rw-r--r--spec/models/project_services/slack_service/merge_message_spec.rb4
-rw-r--r--spec/models/project_services/slack_service/note_message_spec.rb2
-rw-r--r--spec/models/project_services/slack_service/wiki_page_message_spec.rb74
-rw-r--r--spec/models/project_services/slack_service_spec.rb17
-rw-r--r--spec/models/project_spec.rb17
-rw-r--r--spec/models/repository_spec.rb100
-rw-r--r--spec/requests/api/builds_spec.rb2
-rw-r--r--spec/requests/api/commit_status_spec.rb19
-rw-r--r--spec/requests/api/commits_spec.rb6
-rw-r--r--spec/requests/api/licenses_spec.rb136
-rw-r--r--spec/requests/api/tags_spec.rb2
-rw-r--r--spec/requests/ci/api/builds_spec.rb66
-rw-r--r--spec/services/ci/create_builds_service_spec.rb4
-rw-r--r--spec/services/ci/image_for_build_service_spec.rb2
-rw-r--r--spec/services/git_tag_push_service_spec.rb24
-rw-r--r--spec/support/project_hook_data_shared_example.rb17
-rw-r--r--spec/support/repo_helpers.rb2
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb34
309 files changed, 4992 insertions, 1422 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1dc49ca336d..85730e1b687 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -115,6 +115,11 @@ bundler:audit:
script:
- "bundle exec bundle-audit check --update --ignore OSVDB-115941"
+db-migrate-reset:
+ stage: test
+ script:
+ - RAILS_ENV=test bundle exec rake db:migrate:reset
+
# Ruby 2.2 jobs
spec:feature:ruby22:
diff --git a/.rubocop.yml b/.rubocop.yml
index 2fda0b03119..83ed6c38678 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -728,7 +728,7 @@ Metrics/ParameterLists:
# A complexity metric geared towards measuring complexity for a human reader.
Metrics/PerceivedComplexity:
Enabled: true
- Max: 17
+ Max: 18
#################### Lint ################################
diff --git a/CHANGELOG b/CHANGELOG
index 669a26bf042..0be14979df5 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,11 @@
Please view this file on the master branch, on stable branches it's out of date.
+v 8.8.0 (unreleased)
+
v 8.7.0 (unreleased)
+ - Gitlab::GitAccess and Gitlab::GitAccessWiki are now instrumented
+ - The number of InfluxDB points stored per UDP packet can now be configured
+ - Fix error when cross-project label reference used with non-existent project
- Transactions for /internal/allowed now have an "action" tag set
- Method instrumentation now uses Module#prepend instead of aliasing methods
- Repository.clean_old_archives is now instrumented
@@ -11,22 +16,26 @@ v 8.7.0 (unreleased)
- Developers can now add custom tags to transactions
- Loading of an issue's referenced merge requests and related branches is now done asynchronously
- Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea)
+ - Add support to cherry-pick any commit into any branch in the web interface (Minqi Pan)
- Project switcher uses new dropdown styling
- Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea)
- Do not include award_emojis in issue and merge_request comment_count !3610 (Lucas Charles)
- Restrict user profiles when public visibility level is restricted.
+ - Add ability set due date to issues, sort and filter issues by due date (Mehmet Beydogan)
- All images in discussions and wikis now link to their source files !3464 (Connor Shea).
- Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu)
- Add setting for customizing the list of trusted proxies !3524
- Allow projects to be transfered to a lower visibility level group
- Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524
- Improved Markdown rendering performance !3389
+ - Make shared runners text in box configurable
- Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu)
- API: Ability to subscribe and unsubscribe from issues and merge requests (Robert Schilling)
- Expose project badges in project settings
- Make /profile/keys/new redirect to /profile/keys for back-compat. !3717
- Preserve time notes/comments have been updated at when moving issue
- Make HTTP(s) label consistent on clone bar (Stan Hu)
+ - Add support for `after_script`, requires Runner 1.2 (Kamil Trzciński)
- Expose label description in API (Mariusz Jachimowicz)
- API: Ability to update a group (Robert Schilling)
- API: Ability to move issues (Robert Schilling)
@@ -34,6 +43,8 @@ v 8.7.0 (unreleased)
- Fix a bug whith trailing slash in teamcity_url (Charles May)
- Allow back dating on issues when created or updated through the API
- Allow back dating on issue notes when created through the API
+ - Propose license template when creating a new LICENSE file
+ - API: Expose /licenses and /licenses/:key
- Fix avatar stretching by providing a cropping feature
- API: Expose `subscribed` for issues and merge requests (Robert Schilling)
- Allow SAML to handle external users based on user's information !3530
@@ -41,6 +52,7 @@ v 8.7.0 (unreleased)
- Add endpoints to archive or unarchive a project !3372
- Fix a bug whith trailing slash in bamboo_url
- Add links to CI setup documentation from project settings and builds pages
+ - Display project members page to all members
- Handle nil descriptions in Slack issue messages (Stan Hu)
- Add automated repository integrity checks
- API: Expose open_issues_count, closed_issues_count, open_merge_requests_count for labels (Robert Schilling)
@@ -51,6 +63,7 @@ v 8.7.0 (unreleased)
- Use rugged to change HEAD in Project#change_head (P.S.V.R)
- API: Ability to filter milestones by state `active` and `closed` (Robert Schilling)
- API: Fix milestone filtering by `iid` (Robert Schilling)
+ - Make before_script and after_script overridable on per-job (Kamil Trzciński)
- API: Delete notes of issues, snippets, and merge requests (Robert Schilling)
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Better errors handling when creating milestones inside groups
@@ -58,6 +71,7 @@ v 8.7.0 (unreleased)
- Hide `Create a group` help block when creating a new project in a group
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
- Allow issues and merge requests to be assigned to the author !2765
+ - Make Ci::Commit to group only similar builds and make it stateful (ref, tag)
- Gracefully handle notes on deleted commits in merge requests (Stan Hu)
- Decouple membership and notifications
- Fix creation of merge requests for orphaned branches (Stan Hu)
@@ -71,29 +85,48 @@ v 8.7.0 (unreleased)
- API: Do not leak group existence via return code (Robert Schilling)
- ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591
- Update number of Todos in the sidebar when it's marked as "Done". !3600
+ - Sanitize branch names created for confidential issues
- API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling)
- API: User can leave a project through the API when not master or owner. !3613
- Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu)
- Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld)
- Improved markdown forms
+ - Diff design updates (colors, button styles, etc)
+ - Copying and pasting a diff no longer pastes the line numbers or +/-
+ - Add null check to formData when updating profile content to fix Firefox bug
+ - Disable spellcheck and autocorrect for username field in admin page
- Delete tags using Rugged for performance reasons (Robert Schilling)
+ - Add Slack notifications when Wiki is edited (Sebastian Klier)
- Diffs load at the correct point when linking from from number
- Selected diff rows highlight
- Fix emoji categories in the emoji picker
+ - API: Properly display annotated tags for GET /projects/:id/repository/tags (Robert Schilling)
- Add encrypted credentials for imported projects and migrate old ones
+ - Properly format all merge request references with ! rather than # !3740 (Ben Bodenmiller)
- Author and participants are displayed first on users autocompletion
- Show number sign on external issue reference text (Florent Baldino)
- Updated print style for issues
- Use GitHub Issue/PR number as iid to keep references
- Import GitHub labels
- Add option to filter by "Owned projects" on dashboard page
+ - Import GitHub milestones
+ - Fix emoji catgories in the emoji picker
+ - Execute system web hooks on push to the project
+ - Allow enable/disable push events for system hooks
+ - Fix GitHub project's link in the import page when provider has a custom URL
+ - Add RAW build trace output and button on build page
+ - Add incremental build trace update into CI API
+
+v 8.6.7
+ - Fix persistent XSS vulnerability in `commit_person_link` helper
+ - Fix persistent XSS vulnerability in Label and Milestone dropdowns
+ - Fix vulnerability that made it possible to enumerate private projects belonging to group
v 8.6.6
- Expire the exists cache before deletion to ensure project dir actually exists (Stan Hu). !3413
- Fix error on language detection when repository has no HEAD (e.g., master branch) (Jeroen Bobbeldijk). !3654
- Fix revoking of authorized OAuth applications (Connor Shea). !3690
- Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk)
- - Project switcher uses new dropdown styling
- Issuable header is consistent between issues and merge requests
- Improved spacing in issuable header on mobile
@@ -225,6 +258,9 @@ v 8.6.0
- Trigger a todo for mentions on commits page
- Let project owners and admins soft delete issues and merge requests
+v 8.5.11
+ - Fix persistent XSS vulnerability in `commit_person_link` helper
+
v 8.5.10
- Fix a 2FA authentication spoofing vulnerability.
@@ -372,6 +408,9 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos
+v 8.4.9
+ - Fix persistent XSS vulnerability in `commit_person_link` helper
+
v 8.4.8
- Fix a 2FA authentication spoofing vulnerability.
@@ -494,6 +533,9 @@ v 8.4.0
- Add IP check against DNSBLs at account sign-up
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
+v 8.3.8
+ - Fix persistent XSS vulnerability in `commit_person_link` helper
+
v 8.3.7
- Fix a 2FA authentication spoofing vulnerability.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1f26a5d7eaf..24cd5864530 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -323,6 +323,7 @@ request is as follows:
[shell command guidelines](doc/development/shell_commands.md)
1. If your code creates new files on disk please read the
[shared files guidelines](doc/development/shared_files.md).
+1. When writing commit messages please follow [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [guidelines](http://chris.beams.io/posts/git-commit/).
The **official merge window** is in the beginning of the month from the 1st to
the 7th day of the month. This is the best time to submit an MR and get
diff --git a/Gemfile b/Gemfile
index 8caa749a8d4..67cc3f34b8c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -190,6 +190,9 @@ gem 'babosa', '~> 1.0.2'
# Sanitizes SVG input
gem "loofah", "~> 2.0.3"
+# Working with license
+gem 'licensee', '~> 8.0.0'
+
# Protect against bruteforcing
gem "rack-attack", '~> 4.3.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 958824f7ed9..b00d7b35c84 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -452,6 +452,8 @@ GEM
addressable (~> 2.3)
letter_opener (1.1.2)
launchy (~> 2.2)
+ licensee (8.0.0)
+ rugged (>= 0.24b)
listen (3.0.5)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9)
@@ -957,6 +959,7 @@ DEPENDENCIES
jquery-ui-rails (~> 5.0.0)
kaminari (~> 0.16.3)
letter_opener (~> 1.1.2)
+ licensee (~> 8.0.0)
loofah (~> 2.0.3)
mail_room (~> 0.6.1)
method_source (~> 0.8)
diff --git a/PROCESS.md b/PROCESS.md
index cad45d23df9..e34f59c6bce 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -105,6 +105,25 @@ sensitive as to how you word things. Use Emoji to express your feelings (heart,
star, smile, etc.). Some good tips about giving feedback to merge requests is in
the [Thoughtbot code review guide].
+## Feature Freeze
+
+5 working days before the 22nd the stable branches for the upcoming release will
+be frozen for major changes. Merge requests may still be merged into master
+during this period. By freezing the stable branches prior to a release there's
+no need to worry about last minute merge requests potentially breaking a lot of
+things.
+
+What is considered to be a major change is determined on a case by case basis as
+this definition depends very much on the context of changes. For example, a 5
+line change might have a big impact on the entire application. Ultimately the
+decision will be made by those reviewing a merge request and the release
+manager.
+
+During the feature freeze all merge requests that are meant to go into the next
+release should have the correct milestone assigned _and_ have the label
+~"Pick into Stable" set. Merge requests without a milestone and this label will
+not be merged into any stable branches.
+
## Copy & paste responses
### Improperly formatted issue
diff --git a/VERSION b/VERSION
index 91ab1f99daf..d5a967c3933 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.7.0-pre
+8.8.0-pre
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index f3ed9a66715..dd1bbb37551 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -5,6 +5,7 @@
group_projects_path: "/api/:version/groups/:id/projects.json"
projects_path: "/api/:version/projects.json"
labels_path: "/api/:version/projects/:id/labels"
+ license_path: "/api/:version/licenses/:key"
group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path)
@@ -92,6 +93,16 @@
).done (projects) ->
callback(projects)
+ # Return text for a specific license
+ licenseText: (key, data, callback) ->
+ url = Api.buildUrl(Api.license_path).replace(':key', key)
+
+ $.ajax(
+ url: url
+ data: data
+ ).done (license) ->
+ callback(license)
+
buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root?
return url.replace(':version', gon.api_version)
diff --git a/app/assets/javascripts/blob/blob_license_selector.js.coffee b/app/assets/javascripts/blob/blob_license_selector.js.coffee
new file mode 100644
index 00000000000..e17eaa75dc1
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_license_selector.js.coffee
@@ -0,0 +1,30 @@
+class @BlobLicenseSelector
+ licenseRegex: /^(.+\/)?(licen[sc]e|copying)($|\.)/i
+
+ constructor: (editor) ->
+ @$licenseSelector = $('.js-license-selector')
+ $fileNameInput = $('#file_name')
+
+ initialFileNameValue = if $fileNameInput.length
+ $fileNameInput.val()
+ else if $('.editor-file-name').length
+ $('.editor-file-name').text().trim()
+
+ @toggleLicenseSelector(initialFileNameValue)
+
+ if $fileNameInput
+ $fileNameInput.on 'keyup blur', (e) =>
+ @toggleLicenseSelector($(e.target).val())
+
+ $('select.license-select').on 'change', (e) ->
+ data =
+ project: $(this).data('project')
+ fullname: $(this).data('fullname')
+ Api.licenseText $(this).val(), data, (license) ->
+ editor.setValue(license.content, -1)
+
+ toggleLicenseSelector: (fileName) =>
+ if @licenseRegex.test(fileName)
+ @$licenseSelector.show()
+ else
+ @$licenseSelector.hide()
diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee
index 390e41ed8d4..eea9aa972ee 100644
--- a/app/assets/javascripts/blob/edit_blob.js.coffee
+++ b/app/assets/javascripts/blob/edit_blob.js.coffee
@@ -1,44 +1,39 @@
class @EditBlob
- constructor: (assets_path, mode)->
- ace.config.set "modePath", assets_path + '/ace'
+ constructor: (assets_path, ace_mode = null) ->
+ ace.config.set "modePath", "#{assets_path}/ace"
ace.config.loadModule "ace/ext/searchbox"
- if mode
- ace_mode = mode
- editor = ace.edit("editor")
- editor.focus()
- @editor = editor
-
- if ace_mode
- editor.getSession().setMode "ace/mode/" + ace_mode
+ @editor = ace.edit("editor")
+ @editor.focus()
+ @editor.getSession().setMode "ace/mode/#{ace_mode}" if ace_mode
# Before a form submission, move the content from the Ace editor into the
# submitted textarea
- $('form').submit ->
- $("#file-content").val(editor.getValue())
+ $('form').submit =>
+ $("#file-content").val(@editor.getValue())
+
+ @initModePanesAndLinks()
+ new BlobLicenseSelector(@editor)
- editModePanes = $(".js-edit-mode-pane")
- editModeLinks = $(".js-edit-mode a")
- editModeLinks.click (event) ->
- event.preventDefault()
- currentLink = $(this)
- paneId = currentLink.attr("href")
- currentPane = editModePanes.filter(paneId)
- editModeLinks.parent().removeClass "active hover"
- currentLink.parent().addClass "active hover"
- editModePanes.hide()
- if paneId is "#preview"
- currentPane.fadeIn 200
- $.post currentLink.data("preview-url"),
- content: editor.getValue()
- , (response) ->
- currentPane.empty().append response
- currentPane.syntaxHighlight()
- return
+ initModePanesAndLinks: ->
+ @$editModePanes = $(".js-edit-mode-pane")
+ @$editModeLinks = $(".js-edit-mode a")
+ @$editModeLinks.click @editModeLinkClickHandler
- else
- currentPane.fadeIn 200
- editor.focus()
- return
+ editModeLinkClickHandler: (event) =>
+ event.preventDefault()
+ currentLink = $(event.target)
+ paneId = currentLink.attr("href")
+ currentPane = @$editModePanes.filter(paneId)
+ @$editModeLinks.parent().removeClass "active hover"
+ currentLink.parent().addClass "active hover"
+ @$editModePanes.hide()
+ currentPane.fadeIn 200
+ if paneId is "#preview"
+ $.post currentLink.data("preview-url"),
+ content: @editor.getValue()
+ , (response) ->
+ currentPane.empty().append response
+ currentPane.syntaxHighlight()
- editor: ->
- return @editor
+ else
+ @editor.focus()
diff --git a/app/assets/javascripts/blob/new_blob.js.coffee b/app/assets/javascripts/blob/new_blob.js.coffee
deleted file mode 100644
index 68c5e5195e3..00000000000
--- a/app/assets/javascripts/blob/new_blob.js.coffee
+++ /dev/null
@@ -1,20 +0,0 @@
-class @NewBlob
- constructor: (assets_path, mode)->
- ace.config.set "modePath", assets_path + '/ace'
- ace.config.loadModule "ace/ext/searchbox"
- if mode
- ace_mode = mode
- editor = ace.edit("editor")
- editor.focus()
- @editor = editor
-
- if ace_mode
- editor.getSession().setMode "ace/mode/" + ace_mode
-
- # Before a form submission, move the content from the Ace editor into the
- # submitted textarea
- $('form').submit ->
- $("#file-content").val(editor.getValue())
-
- editor: ->
- return @editor
diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee
index ffd3627b1b0..0acb4c1955e 100644
--- a/app/assets/javascripts/commits.js.coffee
+++ b/app/assets/javascripts/commits.js.coffee
@@ -1,7 +1,7 @@
class @CommitsList
@timer = null
- @init: (ref, limit) ->
+ @init: (limit) ->
$("body").on "click", ".day-commits-table li.commit", (event) ->
if event.target.nodeName != "A"
location.href = $(this).attr("url")
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 0b9110d35fa..2fdb7562515 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -17,6 +17,7 @@ class Dispatcher
switch page
when 'projects:issues:index'
Issues.init()
+ Issuable.init()
shortcut_handler = new ShortcutsNavigation()
when 'projects:issues:show'
new Issue()
@@ -57,7 +58,7 @@ class Dispatcher
new ZenMode()
when 'projects:merge_requests:index'
shortcut_handler = new ShortcutsNavigation()
- MergeRequests.init()
+ Issuable.init()
when 'dashboard:activity'
new Activities()
when 'dashboard:projects:starred'
diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee
new file mode 100644
index 00000000000..a4304786cbb
--- /dev/null
+++ b/app/assets/javascripts/due_date_select.js.coffee
@@ -0,0 +1,64 @@
+class @DueDateSelect
+ constructor: ->
+ $loading = $('.js-issuable-update .due_date')
+ .find('.block-loading')
+ .hide()
+
+ $('.js-due-date-select').each (i, dropdown) ->
+ $dropdown = $(dropdown)
+ $dropdownParent = $dropdown.closest('.dropdown')
+ $datePicker = $dropdownParent.find('.js-due-date-calendar')
+ $block = $dropdown.closest('.block')
+ $selectbox = $dropdown.closest('.selectbox')
+ $value = $block.find('.value')
+ $sidebarValue = $('.js-due-date-sidebar-value', $block)
+
+ fieldName = $dropdown.data('field-name')
+ abilityName = $dropdown.data('ability-name')
+ issueUpdateURL = $dropdown.data('issue-update')
+
+ $dropdown.glDropdown(
+ hidden: ->
+ $selectbox.hide()
+ $value.removeAttr('style')
+ )
+
+ addDueDate = ->
+ # Create the post date
+ value = $("input[name='#{fieldName}']").val()
+ date = new Date value.replace(new RegExp('-', 'g'), ',')
+ mediumDate = $.datepicker.formatDate 'M d, yy', date
+
+ data = {}
+ data[abilityName] = {}
+ data[abilityName].due_date = value
+
+ $.ajax(
+ type: 'PUT'
+ url: issueUpdateURL
+ data: data
+ beforeSend: ->
+ $loading.fadeIn()
+ $dropdown.trigger('loading.gl.dropdown')
+ $selectbox.hide()
+ $value.removeAttr('style')
+
+ $value.html(mediumDate)
+ $sidebarValue.html(mediumDate)
+ ).done (data) ->
+ $dropdown.trigger('loaded.gl.dropdown')
+ $dropdown.dropdown('toggle')
+ $loading.fadeOut()
+
+ $datePicker.datepicker(
+ dateFormat: 'yy-mm-dd',
+ defaultDate: $("input[name='#{fieldName}']").val()
+ altField: "input[name='#{fieldName}']"
+ onSelect: ->
+ addDueDate()
+ )
+
+ $(document)
+ .off 'click', '.ui-datepicker-header a'
+ .on 'click', '.ui-datepicker-header a', (e) ->
+ e.stopImmediatePropagation()
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index fac928ef202..29466e9f2ed 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -154,6 +154,9 @@ class GitLabDropdown
@fullData = data
@parseData @fullData
+
+ if @options.filterable
+ @filterInput.trigger 'keyup'
}
# Init filterable
@@ -218,6 +221,9 @@ class GitLabDropdown
menu.toggleClass PAGE_TWO_CLASS
+ # Focus first visible input on active page
+ @dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus()
+
parseData: (data) ->
@renderedData = data
@@ -237,7 +243,8 @@ class GitLabDropdown
shouldPropagate: (e) =>
if @options.multiSelect
$target = $(e.target)
- if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon')
+
+ if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon') and not $target.data('is-link')
e.stopPropagation()
return false
else
@@ -372,7 +379,6 @@ class GitLabDropdown
selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
-
if el.hasClass(ACTIVE_CLASS)
el.removeClass(ACTIVE_CLASS)
field.remove()
@@ -383,13 +389,13 @@ class GitLabDropdown
else
selectedObject
else
- if !value?
- field.remove()
-
- if not @options.multiSelect
+ if not @options.multiSelect or el.hasClass('dropdown-clear-active')
@dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
@dropdown.parent().find("input[name='#{fieldName}']").remove()
+ if !value?
+ field.remove()
+
# Toggle active class for the tick mark
el.addClass ACTIVE_CLASS
diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee
new file mode 100644
index 00000000000..afffed63ac5
--- /dev/null
+++ b/app/assets/javascripts/issuable.js.coffee
@@ -0,0 +1,84 @@
+@Issuable =
+ init: ->
+ Issuable.initTemplates()
+ Issuable.initSearch()
+
+ initTemplates: ->
+ Issuable.labelRow = _.template(
+ '<% _.each(labels, function(label){ %>
+ <span class="label-row">
+ <a href="#"><span class="label color-label has-tooltip" style="background-color: <%= label.color %>; color: <%= label.text_color %>" title="<%= _.escape(label.description) %>" data-container="body"><%= _.escape(label.title) %></span></a>
+ </span>
+ <% }); %>'
+ )
+
+ initSearch: ->
+ @timer = null
+ $('#issue_search')
+ .off 'keyup'
+ .on 'keyup', ->
+ clearTimeout(@timer)
+ @timer = setTimeout( ->
+ Issuable.filterResults $('#issue_search_form')
+ , 500)
+
+ toggleLabelFilters: ->
+ $filteredLabels = $('.filtered-labels')
+ if $filteredLabels.find('.label-row').length > 0
+ $filteredLabels.removeClass('hidden')
+ else
+ $filteredLabels.addClass('hidden')
+
+ filterResults: (form) =>
+ formData = form.serialize()
+
+ $('.issues-holder, .merge-requests-holder').css('opacity', '0.5')
+ formAction = form.attr('action')
+ issuesUrl = formAction
+ issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}")
+ issuesUrl += formData
+ $.ajax
+ type: 'GET'
+ url: formAction
+ data: formData
+ complete: ->
+ $('.issues-holder, .merge-requests-holder').css('opacity', '1.0')
+ success: (data) ->
+ $('.issues-holder, .merge-requests-holder').html(data.html)
+ # Change url so if user reload a page - search results are saved
+ history.replaceState {page: issuesUrl}, document.title, issuesUrl
+ Issuable.reload()
+ Issuable.updateStateFilters()
+ $filteredLabels = $('.filtered-labels')
+
+ if typeof Issuable.labelRow is 'function'
+ $filteredLabels.html(Issuable.labelRow(data))
+
+ Issuable.toggleLabelFilters()
+
+ dataType: "json"
+
+ reload: ->
+ if Issues.created
+ Issues.initChecks()
+
+ $('#filter_issue_search').val($('#issue_search').val())
+
+ updateStateFilters: ->
+ stateFilters = $('.issues-state-filters')
+ newParams = {}
+ paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search']
+
+ for paramKey in paramKeys
+ newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or ''
+
+ if stateFilters.length
+ stateFilters.find('a').each ->
+ initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]')
+ labelNameValues = gl.utils.getParameterValues('label_name[]')
+ if labelNameValues
+ labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&')
+ newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}"
+ else
+ newUrl = gl.utils.mergeUrlParams(newParams, initialUrl)
+ $(this).attr 'href', newUrl
diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee
index 2f19513a831..3a439b94c59 100644
--- a/app/assets/javascripts/issuable_context.js.coffee
+++ b/app/assets/javascripts/issuable_context.js.coffee
@@ -9,21 +9,29 @@ class @IssuableContext
$(".issuable-sidebar .inline-update").on "change", ".js-assignee", ->
$(this).submit()
- $(document).off("click", ".edit-link").on "click",".edit-link", (e) ->
- $block = $(@).parents('.block')
- $selectbox = $block.find('.selectbox')
- if $selectbox.is(':visible')
- $selectbox.hide()
- $block.find('.value').show()
- else
- $selectbox.show()
- $block.find('.value').hide()
-
- if $selectbox.is(':visible')
- setTimeout (->
- $block.find('.dropdown-menu-toggle').trigger 'click'
- ), 0
-
+ $(document)
+ .off 'click', '.dropdown-content a'
+ .on 'click', '.dropdown-content a', (e) ->
+ e.preventDefault()
+
+ $(document)
+ .off 'click', '.edit-link'
+ .on 'click', '.edit-link', (e) ->
+ e.preventDefault()
+
+ $block = $(@).parents('.block')
+ $selectbox = $block.find('.selectbox')
+ if $selectbox.is(':visible')
+ $selectbox.hide()
+ $block.find('.value').show()
+ else
+ $selectbox.show()
+ $block.find('.value').hide()
+
+ if $selectbox.is(':visible')
+ setTimeout ->
+ $block.find('.dropdown-menu-toggle').trigger 'click'
+ , 0
$(".right-sidebar").niceScroll()
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index 0d9f2094c2a..3330e6c68ad 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -1,6 +1,6 @@
@Issues =
init: ->
- Issues.initSearch()
+ Issues.created = true
Issues.initChecks()
$("body").on "ajax:success", ".close_issue, .reopen_issue", ->
@@ -15,10 +15,6 @@
else
$(this).html totalIssues - 1
- reload: ->
- Issues.initChecks()
- $('#filter_issue_search').val($('#issue_search').val())
-
initChecks: ->
$(".check_all_issues").click ->
$(".selected_issue").prop("checked", @checked)
@@ -26,51 +22,6 @@
$(".selected_issue").bind "change", Issues.checkChanged
- # Update state filters if present in page
- updateStateFilters: ->
- stateFilters = $('.issues-state-filters')
- newParams = {}
- paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search']
-
- for paramKey in paramKeys
- newParams[paramKey] = gl.utils.getUrlParameter(paramKey) or ''
-
- if stateFilters.length
- stateFilters.find('a').each ->
- initialUrl = $(this).attr 'href'
- $(this).attr 'href', gl.utils.mergeUrlParams(newParams, initialUrl)
-
- # Make sure we trigger ajax request only after user stop typing
- initSearch: ->
- @timer = null
- $("#issue_search").keyup ->
- clearTimeout(@timer)
- @timer = setTimeout( ->
- Issues.filterResults $("#issue_search_form")
- , 500)
-
- filterResults: (form) =>
- $('.issues-holder, .merge-requests-holder').css("opacity", '0.5')
- formAction = form.attr('action')
- formData = form.serialize()
- issuesUrl = formAction
- issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}")
- issuesUrl += formData
-
- $.ajax
- type: "GET"
- url: formAction
- data: formData
- complete: ->
- $('.issues-holder, .merge-requests-holder').css("opacity", '1.0')
- success: (data) ->
- $('.issues-holder, .merge-requests-holder').html(data.html)
- # Change url so if user reload a page - search results are saved
- history.replaceState {page: issuesUrl}, document.title, issuesUrl
- Issues.reload()
- Issues.updateStateFilters()
- dataType: "json"
-
checkChanged: ->
checked_issues = $(".selected_issue:checked")
if checked_issues.length > 0
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index bc80980acb7..4ac54e13dba 100644
--- a/app/assets/javascripts/labels_select.js.coffee
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -6,7 +6,7 @@ class @LabelsSelect
labelUrl = $dropdown.data('labels')
issueUpdateURL = $dropdown.data('issueUpdate')
selectedLabel = $dropdown.data('selected')
- if selectedLabel?
+ if selectedLabel? and not $dropdown.hasClass 'js-multiselect'
selectedLabel = selectedLabel.split(',')
newLabelField = $('#new_label_name')
newColorField = $('#new_label_color')
@@ -16,33 +16,32 @@ class @LabelsSelect
abilityName = $dropdown.data('ability-name')
$selectbox = $dropdown.closest('.selectbox')
$block = $selectbox.closest('.block')
+ $form = $dropdown.closest('form')
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span')
$value = $block.find('.value')
- $loading = $block.find('.block-loading').fadeOut()
-
- if newLabelField.length
- $newLabelCreateButton = $('.js-new-label-btn')
- $colorPreview = $('.js-dropdown-label-color-preview')
- $newLabelError = $dropdown.parent().find('.js-label-error')
- $newLabelError.hide()
+ $newLabelError = $('.js-label-error')
+ $colorPreview = $('.js-dropdown-label-color-preview')
+ $newLabelCreateButton = $('.js-new-label-btn')
- # Suggested colors in the dropdown to chose from pre-chosen colors
- $('.suggest-colors-dropdown a').on 'click', (e) ->
+ $newLabelError.hide()
+ $loading = $block.find('.block-loading').fadeOut()
issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL?
if issueUpdateURL
labelHTMLTemplate = _.template(
'<% _.each(labels, function(label){ %>
- <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>">
- <span class="label has-tooltip color-label" title="<%= label.description %>" style="background-color: <%= label.color %>;">
- <%= label.title %>
+ <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= _.escape(label.title) %>">
+ <span class="label has-tooltip color-label" title="<%= _.escape(label.description) %>" style="background-color: <%= label.color %>;">
+ <%= _.escape(label.title) %>
</span>
</a>
<% }); %>'
- );
+ )
labelNoneHTMLTemplate = _.template('<div class="light">None</div>')
- if newLabelField.length and $dropdown.hasClass 'js-extra-options'
+ if newLabelField.length
+
+ # Suggested colors in the dropdown to chose from pre-chosen colors
$('.suggest-colors-dropdown a').on "click", (e) ->
e.preventDefault()
e.stopPropagation()
@@ -81,26 +80,25 @@ class @LabelsSelect
enableLabelCreateButton = ->
if newLabelField.val() isnt '' and newColorField.val() isnt ''
$newLabelError.hide()
- $('.js-new-label-btn').disable()
-
- # Create new label with API
- Api.newLabel projectId, {
- name: newLabelField.val()
- color: newColorField.val()
- }, (label) ->
- $('.js-new-label-btn').enable()
-
- if label.message?
- $newLabelError
- .text label.message
- .show()
- else
- $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
-
$newLabelCreateButton.enable()
else
$newLabelCreateButton.disable()
+ saveLabel = ->
+ # Create new label with API
+ Api.newLabel projectId, {
+ name: newLabelField.val()
+ color: newColorField.val()
+ }, (label) ->
+ $newLabelCreateButton.enable()
+
+ if label.message?
+ $newLabelError
+ .text label.message
+ .show()
+ else
+ $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
+
newLabelField.on 'keyup change', enableLabelCreateButton
newColorField.on 'keyup change', enableLabelCreateButton
@@ -111,24 +109,7 @@ class @LabelsSelect
.on 'click', (e) ->
e.preventDefault()
e.stopPropagation()
-
- if newLabelField.val() isnt '' and newColorField.val() isnt ''
- $newLabelError.hide()
- $('.js-new-label-btn').disable()
-
- # Create new label with API
- Api.newLabel projectId, {
- name: newLabelField.val()
- color: newColorField.val()
- }, (label) ->
- $('.js-new-label-btn').enable()
-
- if label.message?
- $newLabelError
- .text label.message
- .show()
- else
- $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
+ saveLabel()
saveLabelData = ->
selected = $dropdown
@@ -171,7 +152,7 @@ class @LabelsSelect
.find('a')
.each((i) ->
setTimeout(=>
- glAnimate($(@), 'pulse')
+ gl.animate.animate($(@), 'pulse')
,200 * i
)
)
@@ -200,18 +181,23 @@ class @LabelsSelect
callback data
renderRow: (label) ->
- selectedClass = ''
- if $selectbox.find("input[type='hidden']\
- [name='#{$dropdown.data('field-name')}']\
- [value='#{label.id}']").length
- selectedClass = 'is-active'
+ removesAll = label.id is 0 or not label.id?
+
+ selectedClass = []
+ if $form.find("input[type='hidden']\
+ [name='#{$dropdown.data('fieldName')}']\
+ [value='#{this.id(label)}']").length
+ selectedClass.push 'is-active'
+
+ if $dropdown.hasClass('js-multiselect') and removesAll
+ selectedClass.push 'dropdown-clear-active'
color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else ""
"<li>
- <a href='#' class='#{selectedClass}'>
+ <a href='#' class='#{selectedClass.join(' ')}'>
#{color}
- #{label.title}
+ #{_.escape(label.title)}
</a>
</li>"
filterable: true
@@ -219,37 +205,56 @@ class @LabelsSelect
fields: ['title']
selectable: true
- toggleLabel: (selected) ->
+ toggleLabel: (selected, el) ->
+ selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active')
+
if selected and selected.title?
- selected.title
+ if selected_labels.length > 1
+ "#{selected.title} +#{selected_labels.length - 1} more"
+ else
+ selected.title
+ else if not selected and selected_labels.length isnt 0
+ if selected_labels.length > 1
+ "#{$(selected_labels[0]).text()} +#{selected_labels.length - 1} more"
+ else if selected_labels.length is 1
+ $(selected_labels).text()
else
defaultLabel
fieldName: $dropdown.data('field-name')
id: (label) ->
- if label.isAny?
- ''
- else if $dropdown.hasClass "js-filter-submit"
+ if $dropdown.hasClass("js-filter-submit") and not label.isAny?
label.title
else
label.id
hidden: ->
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is 'projects:merge_requests:index'
+
$selectbox.hide()
# display:block overrides the hide-collapse rule
$value.removeAttr('style')
if $dropdown.hasClass 'js-multiselect'
- saveLabelData()
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ selectedLabels = $dropdown
+ .closest('form')
+ .find("input:hidden[name='#{$dropdown.data('fieldName')}']")
+ Issuable.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass('js-filter-submit')
+ $dropdown.closest('form').submit()
+ else
+ saveLabelData()
multiSelect: $dropdown.hasClass 'js-multiselect'
clicked: (label) ->
page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index'
- isMRIndex = page is page is 'projects:merge_requests:index'
-
+ isMRIndex = page is 'projects:merge_requests:index'
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
- selectedLabel = label.title
-
- Issues.filterResults $dropdown.closest('form')
+ if not $dropdown.hasClass 'js-multiselect'
+ selectedLabel = label.title
+ Issuable.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit()
else
diff --git a/app/assets/javascripts/lib/animate.js.coffee b/app/assets/javascripts/lib/animate.js.coffee
index 8f892b5a2b9..ec3b44d6126 100644
--- a/app/assets/javascripts/lib/animate.js.coffee
+++ b/app/assets/javascripts/lib/animate.js.coffee
@@ -1,13 +1,39 @@
((w) ->
+ if not w.gl? then w.gl = {}
+ if not gl.animate? then gl.animate = {}
- w.glAnimate = ($el, animation, done) ->
+ gl.animate.animate = ($el, animation, options, done) ->
+ if options?.cssStart?
+ $el.css(options.cssStart)
$el
- .removeClass()
+ .removeClass(animation + ' animated')
.addClass(animation + ' animated')
.one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', ->
- $(this).removeClass()
+ $(this).removeClass(animation + ' animated')
+ if done?
+ done()
+ if options?.cssEnd?
+ $el.css(options.cssEnd)
return
return
- return
+ gl.animate.animateEach = ($els, animation, time, options, done) ->
+ dfd = $.Deferred()
+ if not $els.length
+ dfd.resolve()
+ $els.each((i) ->
+ setTimeout(=>
+ $this = $(@)
+ gl.animate.animate($this, animation, options, =>
+ if i is $els.length - 1
+ dfd.resolve()
+ if done?
+ done()
+ )
+ ,time * i
+ )
+ return
+ )
+ return dfd.promise()
+ return
) window \ No newline at end of file
diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/url_utility.js.coffee
index abd556e0b4e..6a00932c028 100644
--- a/app/assets/javascripts/lib/url_utility.js.coffee
+++ b/app/assets/javascripts/lib/url_utility.js.coffee
@@ -3,16 +3,20 @@
w.gl ?= {}
w.gl.utils ?= {}
- w.gl.utils.getUrlParameter = (sParam) ->
+ # Returns an array containing the value(s) of the
+ # of the key passed as an argument
+ w.gl.utils.getParameterValues = (sParam) ->
sPageURL = decodeURIComponent(window.location.search.substring(1))
sURLVariables = sPageURL.split('&')
sParameterName = undefined
+ values = []
i = 0
while i < sURLVariables.length
sParameterName = sURLVariables[i].split('=')
if sParameterName[0] is sParam
- return if sParameterName[1] is undefined then true else sParameterName[1]
+ values.push(sParameterName[1])
i++
+ values
# #
# @param {Object} params - url keys and value to merge
@@ -28,4 +32,12 @@
newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}"
newUrl
+ # removes parameter query string from url. returns the modified url
+ w.gl.utils.removeParamQueryString = (url, param) ->
+ url = decodeURIComponent(url)
+ urlVariables = url.split('&')
+ (
+ variables for variables in urlVariables when variables.indexOf(param) is -1
+ ).join('&')
+
) window
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index e8613cab72b..372732d0aac 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -87,8 +87,8 @@ class @MergeRequestTabs
if window.location.hash
navBarHeight = $('.navbar-gitlab').outerHeight()
- $el = $("#{container} #{window.location.hash}")
- $.scrollTo("#{container} #{window.location.hash}", offset: -navBarHeight) if $el.length
+ $el = $("#{container} #{window.location.hash}:not(.match)")
+ $.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length
# Activate a tab based on the current action
activateTab: (action) ->
@@ -176,12 +176,12 @@ class @MergeRequestTabs
if locationHash isnt ''
hashClassString = ".#{locationHash.replace('#', '')}"
- $diffLine = $(locationHash)
+ $diffLine = $("#{locationHash}:not(.match)", $('#diffs'))
- if $diffLine.is ':not(tr)'
- $diffLine = $("td#{locationHash}, td#{hashClassString}")
+ if not $diffLine.is 'tr'
+ $diffLine = $('#diffs').find("td#{locationHash}, td#{hashClassString}")
else
- $diffLine = $('td', $diffLine)
+ $diffLine = $diffLine.find('td')
if $diffLine.length
$diffLine.addClass 'hll'
diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee
deleted file mode 100644
index b3c73ffce5d..00000000000
--- a/app/assets/javascripts/merge_requests.js.coffee
+++ /dev/null
@@ -1,35 +0,0 @@
-#
-# * Filter merge requests
-#
-@MergeRequests =
- init: ->
- MergeRequests.initSearch()
-
- # Make sure we trigger ajax request only after user stop typing
- initSearch: ->
- @timer = null
- $("#issue_search").keyup ->
- clearTimeout(@timer)
- @timer = setTimeout(MergeRequests.filterResults, 500)
-
- filterResults: =>
- form = $("#issue_search_form")
- search = $("#issue_search").val()
- $('.merge-requests-holder').css("opacity", '0.5')
- issues_url = form.attr('action') + '?' + form.serialize()
-
- $.ajax
- type: "GET"
- url: form.attr('action')
- data: form.serialize()
- complete: ->
- $('.merge-requests-holder').css("opacity", '1.0')
- success: (data) ->
- $('.merge-requests-holder').html(data.html)
- # Change url so if user reload a page - search results are saved
- history.replaceState {page: issues_url}, document.title, issues_url
- MergeRequests.reload()
- dataType: "json"
-
- reload: ->
- $('#filter_issue_search').val($('#issue_search').val())
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
index 6bd4e885a03..345a0e447af 100644
--- a/app/assets/javascripts/milestone_select.js.coffee
+++ b/app/assets/javascripts/milestone_select.js.coffee
@@ -24,7 +24,7 @@ class @MilestoneSelect
if issueUpdateURL
milestoneLinkTemplate = _.template(
- '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= title %></a>'
+ '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= _.escape(title) %></a>'
)
milestoneLinkNoneTemplate = '<div class="light">None</div>'
@@ -71,7 +71,7 @@ class @MilestoneSelect
defaultLabel
fieldName: $dropdown.data('field-name')
text: (milestone) ->
- milestone.title
+ _.escape(milestone.title)
id: (milestone) ->
if !useId
milestone.name
@@ -97,7 +97,7 @@ class @MilestoneSelect
selectedMilestone = selected.name
else
selectedMilestone = ''
- Issues.filterResults $dropdown.closest('form')
+ Issuable.filterResults $dropdown.closest('form')
else if $dropdown.hasClass('js-filter-submit')
$dropdown.closest('form').submit()
else
diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee
index 00d2b641723..10e698d6a54 100644
--- a/app/assets/javascripts/todos.js.coffee
+++ b/app/assets/javascripts/todos.js.coffee
@@ -1,5 +1,11 @@
class @Todos
- constructor: (@name) ->
+ constructor: (opts = {}) ->
+ {
+ @el = $('.js-todos-options')
+ } = opts
+
+ @perPage = @el.data('perPage')
+
@clearListeners()
@initBtnListeners()
@@ -26,6 +32,7 @@ class @Todos
dataType: 'json'
data: '_method': 'delete'
success: (data) =>
+ @redirectIfNeeded data.count
@clearDone $this.closest('li')
@updateBadges data
@@ -57,6 +64,40 @@ class @Todos
$('.todos-pending .badge, .todos-pending-count').text data.count
$('.todos-done .badge').text data.done_count
+ getTotalPages: ->
+ @el.data('totalPages')
+
+ getCurrentPage: ->
+ @el.data('currentPage')
+
+ getTodosPerPage: ->
+ @el.data('perPage')
+
+ redirectIfNeeded: (total) ->
+ currPages = @getTotalPages()
+ currPage = @getCurrentPage()
+
+ # Refresh if no remaining Todos
+ if not total
+ location.reload()
+ return
+
+ # Do nothing if no pagination
+ return if not currPages
+
+ newPages = Math.ceil(total / @getTodosPerPage())
+ url = location.href # Includes query strings
+
+ # If new total of pages is different than we have now
+ if newPages isnt currPages
+ # Redirect to previous page if there's one available
+ if currPages > 1 and currPage is currPages
+ pageParams =
+ page: currPages - 1
+ url = gl.utils.mergeUrlParams(pageParams, url)
+
+ Turbolinks.visit(url)
+
goToTodoUrl: (e)->
todoLink = $(this).data('url')
return unless todoLink
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index a7e934936e9..b80b1b861cc 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -158,7 +158,7 @@ class @UsersSelect
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
selectedId = user.id
- Issues.filterResults $dropdown.closest('form')
+ Issuable.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit()
else
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index ba6c7930cdc..239eaf15cc1 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -248,7 +248,7 @@
.dropdown-title {
position: relative;
- padding: 0 25px 15px;
+ padding: 0 25px 10px;
margin: 0 10px 10px;
font-weight: 600;
line-height: 1;
@@ -278,7 +278,7 @@
right: 5px;
width: 20px;
height: 20px;
- top: -1px;
+ top: -3px;
}
.dropdown-menu-back {
@@ -320,7 +320,7 @@
}
}
-.dropdown-input-field {
+.dropdown-input-field, .default-dropdown-input {
width: 100%;
padding: 0 7px;
color: $dropdown-input-color;
@@ -358,6 +358,13 @@
border-top: 1px solid $dropdown-divider-color;
}
+.dropdown-due-date-footer {
+ padding-top: 0;
+ margin-left: 10px;
+ margin-right: 10px;
+ border-top: 0;
+}
+
.dropdown-footer-list {
font-size: 14px;
@@ -395,3 +402,122 @@
height: 15px;
border-radius: $border-radius-base;
}
+
+.dropdown-menu-due-date {
+ .dropdown-content {
+ max-height: 230px;
+ }
+
+ .ui-widget {
+ table {
+ margin: 0;
+ }
+
+ &.ui-datepicker-inline {
+ padding: 0 10px;
+ border: 0;
+ width: 100%;
+ }
+
+ .ui-datepicker-header {
+ padding: 0 8px 10px;
+ border: 0;
+
+ .ui-icon {
+ background: none;
+ font-size: 20px;
+ text-indent: 0;
+
+ &:before {
+ display: block;
+ position: relative;
+ top: -2px;
+ color: $dropdown-title-btn-color;
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+ }
+ }
+
+ .ui-state-active,
+ .ui-state-hover {
+ color: $md-link-color;
+ background-color: $calendar-hover-bg;
+ }
+
+ .ui-datepicker-prev,
+ .ui-datepicker-next {
+ top: 0;
+ height: 15px;
+ cursor: pointer;
+
+ &:hover {
+ background-color: transparent;
+ border: 0;
+
+ .ui-icon:before {
+ color: $md-link-color;
+ }
+ }
+ }
+
+ .ui-datepicker-prev {
+ left: 0;
+
+ .ui-icon:before {
+ content: '\f104';
+ text-align: left;
+ }
+ }
+
+ .ui-datepicker-next {
+ right: 0;
+
+ .ui-icon:before {
+ content: '\f105';
+ text-align: right;
+ }
+ }
+
+ td {
+ padding: 0;
+ border: 1px solid $calendar-border-color;
+
+ &:first-child {
+ border-left: 0;
+ }
+
+ &:last-child {
+ border-right: 0;
+ }
+
+ a {
+ line-height: 17px;
+ border: 0;
+ border-radius: 0;
+ }
+ }
+
+ .ui-datepicker-title {
+ color: $gl-gray;
+ font-size: 15px;
+ line-height: 1;
+ font-weight: normal;
+ }
+ }
+
+ th {
+ padding: 2px 0;
+ color: $calendar-header-color;
+ font-weight: normal;
+ text-transform: lowercase;
+ border-top: 1px solid $calendar-border-color;
+ }
+
+ .ui-datepicker-unselectable {
+ background-color: $calendar-unselectable-bg;
+ }
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 789df42fb66..148a7cab6e2 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -38,12 +38,14 @@
.filename {
&.old {
+ display: inline-block;
span.idiff {
background-color: #f8cbcb;
}
}
&.new {
+ display: inline-block;
span.idiff {
background-color: #a6f3a6;
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 0f32d36d59c..704fa1ff800 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -90,3 +90,12 @@
box-shadow: none;
width: 100%;
}
+
+.md {
+ &.md-preview-holder {
+ code {
+ white-space: pre-wrap;
+ word-break: break-all;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index f910cf61817..30ca27ab104 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -241,3 +241,8 @@ $note-form-border-color: #e5e5e5;
$note-toolbar-color: #959494;
$zen-control-hover-color: #111;
+
+$calendar-header-color: #b8b8b8;
+$calendar-hover-bg: #ecf3fe;
+$calendar-border-color: rgba(#000, .1);
+$calendar-unselectable-bg: #faf9f9;
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 2c2ac903f29..751a5ab4d92 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -36,4 +36,10 @@
}
}
}
+
+ .wiki {
+ code {
+ white-space: pre-wrap;
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 183f22a1b24..e7c8198ba45 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -34,6 +34,7 @@
background: #fff;
color: #333;
border-radius: 0 0 3px 3px;
+ -webkit-overflow-scrolling: auto;
.unfold {
cursor: pointer;
@@ -86,7 +87,7 @@
}
span {
- white-space: pre;
+ white-space: pre-wrap;
}
}
}
@@ -335,7 +336,7 @@
}
.diff-file .line_content {
- white-space: pre;
+ white-space: pre-wrap;
}
.diff-wrap-lines .line_content {
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 0f0592a0ab8..8981f070a20 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -26,6 +26,10 @@
line-height: 42px;
padding-top: 7px;
padding-bottom: 7px;
+
+ .pull-right {
+ height: 20px;
+ }
}
.editor-ref {
@@ -53,4 +57,9 @@
.select2 {
float: right;
}
+
+ .encoding-selector,
+ .license-selector {
+ display: inline-block;
+ }
}
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index 604f1700cf8..ee95bdf488e 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -59,8 +59,10 @@
position: relative;
overflow-y: auto;
padding: 15px;
+
.form-actions {
margin: -$gl-padding+1;
+ margin-top: 15px;
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 5bf44c1cdb6..1cf3023ecc9 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -128,6 +128,7 @@
top: 58px;
bottom: 0;
right: 0;
+ z-index: 10;
transition: width .3s;
background: $gray-light;
padding: 10px 20px;
@@ -241,16 +242,20 @@
}
}
- .btn {
+ .issuable-pager {
background: $gray-normal;
border: 1px solid $border-gray-normal;
&:hover {
background: $gray-dark;
border: 1px solid $border-gray-dark;
}
+
+ &.btn-primary {
+ @extend .btn-primary
+ }
}
- a:not(.btn) {
+ a:not(.issuable-pager) {
&:hover {
color: $md-link-color;
text-decoration: none;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index c2371d79989..7fa13e66b43 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -42,6 +42,7 @@
.note-textarea {
display: block;
padding: 10px 0;
+ color: $gl-gray;
font-family: $regular_font;
border: 0;
@@ -83,18 +84,6 @@
border-color: $gl-success;
}
}
-
- p {
- code {
- white-space: normal;
- }
-
- pre {
- code {
- white-space: pre;
- }
- }
- }
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index ffcd89a3c9f..d54abe9bc02 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -81,16 +81,8 @@ ul.notes {
@include md-typography;
// On diffs code should wrap nicely and not overflow
- p {
- code {
- white-space: normal;
- }
-
- pre {
- code {
- white-space: pre;
- }
- }
+ code {
+ white-space: pre-wrap;
}
// Reset ul style types since we're nested inside a ul already
@@ -137,7 +129,7 @@ ul.notes {
margin-right: 10px;
}
.line_content {
- white-space: pre;
+ white-space: pre-wrap;
}
}
@@ -171,11 +163,6 @@ ul.notes {
&.parallel {
border-width: 1px;
-
- .code,
- code {
- white-space: pre-wrap;
- }
}
.notes {
@@ -196,6 +183,9 @@ ul.notes {
}
}
+ .author_link {
+ color: $gl-gray;
+ }
}
.note-headline-light,
@@ -308,7 +298,7 @@ ul.notes {
padding: 4px;
font-size: 16px;
color: $gl-link-color;
- margin-left: -60px;
+ margin-left: -56px;
position: absolute;
z-index: 10;
width: 32px;
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index b4a28b8dd3f..ec22548ddeb 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -75,6 +75,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:admin_notification_email,
:user_oauth_applications,
:shared_runners_enabled,
+ :shared_runners_text,
:max_artifacts_size,
:metrics_enabled,
:metrics_host,
@@ -92,6 +93,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:akismet_api_key,
:email_author_in_body,
:repository_checks_enabled,
+ :metrics_packet_size,
restricted_visibility_levels: [],
import_sources: []
)
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index 0bd19c49d8f..93c4894ea0f 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -39,6 +39,6 @@ class Admin::HooksController < Admin::ApplicationController
end
def hook_params
- params.require(:hook).permit(:url, :enable_ssl_verification)
+ params.require(:hook).permit(:url, :enable_ssl_verification, :push_events, :tag_push_events)
end
end
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index f159e169f6d..b8b9e78427d 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -1,7 +1,7 @@
class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry]
- before_action :authorize_update_build!, except: [:index, :show, :status]
+ before_action :authorize_update_build!, except: [:index, :show, :status, :raw]
layout 'project'
def index
@@ -62,6 +62,14 @@ class Projects::BuildsController < Projects::ApplicationController
notice: "Build has been sucessfully erased!"
end
+ def raw
+ if @build.has_trace?
+ send_file @build.path_to_trace, type: 'text/plain; charset=utf-8', disposition: 'inline'
+ else
+ render_404
+ end
+ end
+
private
def build
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 576fa3cedb2..a202cb38692 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -12,7 +12,7 @@ class Projects::CommitController < Projects::ApplicationController
before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit
before_action :define_show_vars, only: [:show, :builds]
- before_action :authorize_edit_tree!, only: [:revert]
+ before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
def show
apply_diff_view_cookie!
@@ -38,13 +38,13 @@ class Projects::CommitController < Projects::ApplicationController
end
def cancel_builds
- ci_commit.builds.running_or_pending.each(&:cancel)
+ ci_builds.running_or_pending.each(&:cancel)
redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha)
end
def retry_builds
- ci_commit.builds.latest.failed.each do |build|
+ ci_builds.latest.failed.each do |build|
if build.retryable?
Ci::Build.retry(build)
end
@@ -60,27 +60,32 @@ class Projects::CommitController < Projects::ApplicationController
end
def revert
- assign_revert_commit_vars
+ assign_change_commit_vars(@commit.revert_branch_name)
return render_404 if @target_branch.blank?
- create_commit(Commits::RevertService, success_notice: "The #{revert_type_title} has been successfully reverted.",
- success_path: successful_revert_path, failure_path: failed_revert_path)
+ create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.",
+ success_path: successful_change_path, failure_path: failed_change_path)
end
+
+ def cherry_pick
+ assign_change_commit_vars(@commit.cherry_pick_branch_name)
+
+ return render_404 if @target_branch.blank?
- private
-
- def revert_type_title
- @commit.merged_merge_request ? 'merge request' : 'commit'
+ create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.",
+ success_path: successful_change_path, failure_path: failed_change_path)
end
- def successful_revert_path
+ private
+
+ def successful_change_path
return referenced_merge_request_url if @commit.merged_merge_request
namespace_project_commits_url(@project.namespace, @project, @target_branch)
end
- def failed_revert_path
+ def failed_change_path
return referenced_merge_request_url if @commit.merged_merge_request
namespace_project_commit_url(@project.namespace, @project, params[:id])
@@ -94,8 +99,12 @@ class Projects::CommitController < Projects::ApplicationController
@commit ||= @project.commit(params[:id])
end
- def ci_commit
- @ci_commit ||= project.ci_commit(commit.sha)
+ def ci_commits
+ @ci_commits ||= project.ci_commits.where(sha: commit.sha)
+ end
+
+ def ci_builds
+ @ci_builds ||= Ci::Build.where(commit: ci_commits)
end
def define_show_vars
@@ -108,17 +117,17 @@ class Projects::CommitController < Projects::ApplicationController
@diff_refs = [commit.parent || commit, commit]
@notes_count = commit.notes.count
- @statuses = ci_commit.statuses if ci_commit
+ @statuses = CommitStatus.where(commit: ci_commits)
+ @builds = Ci::Build.where(commit: ci_commits)
end
- def assign_revert_commit_vars
+ def assign_change_commit_vars(mr_source_branch)
@commit = project.commit(params[:id])
@target_branch = params[:target_branch]
- @mr_source_branch = @commit.revert_branch_name
+ @mr_source_branch = mr_source_branch
@mr_target_branch = @target_branch
@commit_params = {
commit: @commit,
- revert_type_title: revert_type_title,
create_merge_request: params[:create_merge_request].present? || different_project?
}
end
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 4159e53bfa9..606552fa853 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -7,10 +7,12 @@ class Projects::GroupLinksController < Projects::ApplicationController
end
def create
- link = project.project_group_links.new
- link.group_id = params[:link_group_id]
- link.group_access = params[:link_group_access]
- link.save
+ group = Group.find(params[:link_group_id])
+ return render_404 unless can?(current_user, :read_group, group)
+
+ project.project_group_links.create(
+ group: group, group_access: params[:link_group_access]
+ )
redirect_to namespace_project_group_links_path(project.namespace, project)
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 38214f04793..b96ab91c17d 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -33,14 +33,15 @@ class Projects::IssuesController < Projects::ApplicationController
end
@issues = @issues.page(params[:page])
- @label = @project.labels.find_by(title: params[:label_name])
+ @labels = @project.labels.where(title: params[:label_name])
respond_to do |format|
format.html
format.atom { render layout: false }
format.json do
render json: {
- html: view_to_html_string("projects/issues/_issues")
+ html: view_to_html_string("projects/issues/_issues"),
+ labels: @labels.as_json(methods: :text_color)
}
end
end
@@ -128,10 +129,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def related_branches
- merge_requests = @issue.referenced_merge_requests(current_user)
-
- @related_branches = @issue.related_branches -
- merge_requests.map(&:source_branch)
+ @related_branches = @issue.related_branches(current_user)
respond_to do |format|
format.json do
@@ -194,7 +192,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
:title, :assignee_id, :position, :description, :confidential,
- :milestone_id, :state_event, :task_num, label_ids: []
+ :milestone_id, :due_date, :state_event, :task_num, label_ids: []
)
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 3e0cfc6aa65..81003c34f59 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -38,13 +38,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project)
- @label = @project.labels.find_by(title: params[:label_name])
+ @labels = @project.labels.where(title: params[:label_name])
respond_to do |format|
format.html
format.json do
render json: {
- html: view_to_html_string("projects/merge_requests/_merge_requests")
+ html: view_to_html_string("projects/merge_requests/_merge_requests"),
+ labels: @labels.as_json(methods: :text_color)
}
end
end
@@ -320,6 +321,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_widget_vars
@ci_commit = @merge_request.ci_commit
+ @ci_commits = [@ci_commit].compact
closes_issues
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index e457db2f0b7..33b2625c0ac 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -1,6 +1,6 @@
class Projects::ProjectMembersController < Projects::ApplicationController
# Authorize
- before_action :authorize_admin_project_member!, except: :leave
+ before_action :authorize_admin_project_member!, except: [:leave, :index]
def index
@project_members = @project.project_members
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 8b2577aebe1..739681f4085 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -6,7 +6,7 @@ class Projects::ServicesController < Projects::ApplicationController
:description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
:colorize_messages, :channels,
:push_events, :issues_events, :merge_requests_events, :tag_push_events,
- :note_events, :build_events,
+ :note_events, :build_events, :wiki_page_events,
:notify_only_broken_builds, :add_pusher,
:send_from_committer_email, :disable_diffs, :external_wiki_url,
:notify, :color,
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 9f3a4a69721..c02bc28acef 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -44,7 +44,7 @@ class Projects::WikisController < Projects::ApplicationController
return render('empty') unless can?(current_user, :create_wiki, @project)
- if @page.update(content, format, message)
+ if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
redirect_to(
namespace_project_wiki_path(@project.namespace, @project, @page),
notice: 'Wiki was successfully updated.'
@@ -55,9 +55,9 @@ class Projects::WikisController < Projects::ApplicationController
end
def create
- @page = WikiPage.new(@project_wiki)
+ @page = WikiPages::CreateService.new(@project, current_user, wiki_params).execute
- if @page.create(wiki_params)
+ if @page.persisted?
redirect_to(
namespace_project_wiki_path(@project.namespace, @project, @page),
notice: 'Wiki was successfully updated.'
@@ -122,15 +122,4 @@ class Projects::WikisController < Projects::ApplicationController
params[:wiki].slice(:title, :content, :format, :message)
end
- def content
- params[:wiki][:content]
- end
-
- def format
- params[:wiki][:format]
- end
-
- def message
- params[:wiki][:message]
- end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index f1df6832bf6..93aa30b3255 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -39,6 +39,7 @@ class IssuableFinder
items = by_assignee(items)
items = by_author(items)
items = by_label(items)
+ items = by_due_date(items)
sort(items)
end
@@ -117,7 +118,7 @@ class IssuableFinder
end
def filter_by_no_label?
- labels? && params[:label_name] == Label::None.title
+ labels? && params[:label_name].include?(Label::None.title)
end
def labels
@@ -271,18 +272,55 @@ class IssuableFinder
items = items.without_label
else
items = items.with_label(label_names)
-
if projects
items = items.where(labels: { project_id: projects })
end
end
end
+ # When filtering by multiple labels we may end up duplicating issues (if one
+ # has multiple labels). This ensures we only return unique issues.
+ items.distinct
+ end
+
+ def by_due_date(items)
+ if due_date?
+ if filter_by_no_due_date?
+ items = items.without_due_date
+ elsif filter_by_overdue?
+ items = items.due_before(Date.today)
+ elsif filter_by_due_this_week?
+ items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week)
+ elsif filter_by_due_this_month?
+ items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month)
+ end
+ end
+
items
end
+ def filter_by_no_due_date?
+ due_date? && params[:due_date] == Issue::NoDueDate.name
+ end
+
+ def filter_by_overdue?
+ due_date? && params[:due_date] == Issue::Overdue.name
+ end
+
+ def filter_by_due_this_week?
+ due_date? && params[:due_date] == Issue::DueThisWeek.name
+ end
+
+ def filter_by_due_this_month?
+ due_date? && params[:due_date] == Issue::DueThisMonth.name
+ end
+
+ def due_date?
+ params[:due_date].present? && klass.column_names.include?('due_date')
+ end
+
def label_names
- params[:label_name].split(',')
+ params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
end
def current_user_related?
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 16e5b8ac223..3e0074da394 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -254,11 +254,11 @@ module ApplicationHelper
def page_filter_path(options = {})
without = options.delete(:without)
+ add_label = options.delete(:label)
exist_opts = {
state: params[:state],
scope: params[:scope],
- label_name: params[:label_name],
milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id],
author_id: params[:author_id],
@@ -275,6 +275,13 @@ module ApplicationHelper
path = request.path
path << "?#{options.to_param}"
+ if add_label
+ if params[:label_name].present? and params[:label_name].respond_to?('any?')
+ params[:label_name].each do |label|
+ path << "&label_name[]=#{label}"
+ end
+ end
+ end
path
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 60a0ff32c9c..914b0ef6042 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -15,6 +15,10 @@ module ApplicationSettingsHelper
current_application_settings.sign_in_text
end
+ def shared_runners_text
+ current_application_settings.shared_runners_text
+ end
+
def user_oauth_applications?
current_application_settings.user_oauth_applications
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 9e59a295fc4..a4d7c425d0f 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -173,4 +173,15 @@ module BlobHelper
response.etag = @blob.id
!stale
end
+
+ def licenses_for_select
+ return @licenses_for_select if defined?(@licenses_for_select)
+
+ licenses = Licensee::License.all
+
+ @licenses_for_select = {
+ Popular: licenses.select(&:featured).map { |license| [license.name, license.key] },
+ Other: licenses.reject(&:featured).map { |license| [license.name, license.key] }
+ }
+ end
end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 8b1575d5e0c..417050b4132 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -4,14 +4,6 @@ module CiStatusHelper
builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha)
end
- def ci_status_icon(ci_commit)
- ci_icon_for_status(ci_commit.status)
- end
-
- def ci_status_label(ci_commit)
- ci_label_for_status(ci_commit.status)
- end
-
def ci_status_with_icon(status, target = nil)
content = ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
klass = "ci-status ci-#{status}"
@@ -47,10 +39,13 @@ module CiStatusHelper
end
def render_ci_status(ci_commit, tooltip_placement: 'auto left')
- link_to ci_status_icon(ci_commit),
+ # TODO: split this method into
+ # - render_commit_status
+ # - render_pipeline_status
+ link_to ci_icon_for_status(ci_commit.status),
ci_status_path(ci_commit),
class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}",
- title: "Build #{ci_status_label(ci_commit)}",
+ title: "Build #{ci_label_for_status(ci_commit.status)}",
data: { toggle: 'tooltip', placement: tooltip_placement }
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 35ba543cef1..b59c3982edd 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -126,12 +126,10 @@ module CommitsHelper
def revert_commit_link(commit, continue_to_path, btn_class: nil)
return unless current_user
- tooltip = "Revert this #{revert_commit_type(commit)} in a new merge request"
+ tooltip = "Revert this #{commit.change_type_title} in a new merge request"
if can_collaborate_with_project?
- content_tag :span, 'data-toggle' => 'modal', 'data-target' => '#modal-revert-commit' do
- link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class}"
- end
+ link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip"
elsif can?(current_user, :fork_project, @project)
continue_params = {
to: continue_to_path,
@@ -146,11 +144,24 @@ module CommitsHelper
end
end
- def revert_commit_type(commit)
- if commit.merged_merge_request
- 'merge request'
- else
- 'commit'
+ def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil)
+ return unless current_user
+
+ tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request"
+
+ if can_collaborate_with_project?
+ link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip"
+ elsif can?(current_user, :fork_project, @project)
+ continue_params = {
+ to: continue_to_path,
+ notice: edit_in_new_fork_notice + ' Try to cherry-pick this commit again.',
+ notice_now: edit_in_new_fork_notice_now
+ }
+ fork_path = namespace_project_forks_path(@project.namespace, @project,
+ namespace_key: current_user.namespace.id,
+ continue: continue_params)
+
+ link_to 'Cherry-pick', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip
end
end
@@ -183,7 +194,7 @@ module CommitsHelper
options = {
class: "commit-#{options[:source]}-link has-tooltip",
- data: { 'original-title'.to_sym => sanitize(source_email) }
+ title: source_email
}
if user.nil?
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index f3fddef01cb..f07eff3fb57 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -25,6 +25,10 @@ module GitlabRoutingHelper
namespace_project_commits_path(project.namespace, project, @ref || project.repository.root_ref)
end
+ def project_pipelines_path(project, *args)
+ namespace_project_pipelines_path(project.namespace, project, *args)
+ end
+
def project_builds_path(project, *args)
namespace_project_builds_path(project.namespace, project, *args)
end
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
new file mode 100644
index 00000000000..109bc1a02d1
--- /dev/null
+++ b/app/helpers/import_helper.rb
@@ -0,0 +1,18 @@
+module ImportHelper
+ def github_project_link(path_with_namespace)
+ link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank'
+ end
+
+ private
+
+ def github_project_url(path_with_namespace)
+ "#{github_root_url}/#{path_with_namespace}"
+ end
+
+ def github_root_url
+ return @github_url if defined?(@github_url)
+
+ provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' }
+ @github_url = provider.fetch('url', 'https://github.com') if provider
+ end
+end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 49b6b79ce35..39474217286 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -16,6 +16,25 @@ module IssuablesHelper
base_issuable_scope(issuable).where('iid > ?', issuable.iid).last
end
+ def multi_label_name(current_labels, default_label)
+ # current_labels may be a string from before
+ if current_labels.is_a?(Array)
+ if current_labels.count > 1
+ "#{current_labels[0]} +#{current_labels.count - 1} more"
+ else
+ current_labels[0]
+ end
+ elsif current_labels.is_a?(String)
+ if current_labels.nil? || current_labels.empty?
+ default_label
+ else
+ current_labels
+ end
+ else
+ default_label
+ end
+ end
+
def issuable_json_path(issuable)
project = issuable.project
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 4cb8adcebad..afe1e11a0da 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -131,7 +131,7 @@ module IssuesHelper
class: "icon emoji-icon emoji-#{unicode}",
title: name,
data: data
- else
+ else
# Emoji icons displayed separately, used for the awards already given
# to an issue or merge request.
content_tag :img, "",
@@ -172,6 +172,18 @@ module IssuesHelper
end.to_h
end
+ def due_date_options
+ options = [
+ Issue::AnyDueDate,
+ Issue::NoDueDate,
+ Issue::DueThisWeek,
+ Issue::DueThisMonth,
+ Issue::Overdue
+ ]
+
+ options_from_collection_for_select(options, 'name', 'title', params[:due_date])
+ end
+
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 82f805fa444..e4e8b934bc8 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -84,6 +84,14 @@ module PageLayoutHelper
end
end
+ def nav(name = nil)
+ if name
+ @nav = name
+ else
+ @nav
+ end
+ end
+
def fluid_layout(enabled = false)
if @fluid_layout.nil?
@fluid_layout = (current_user && current_user.layout == "fluid") || enabled
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 7e00aacceaa..ab85694da3f 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -52,7 +52,7 @@ module ProjectsHelper
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
else
title = opts[:title].sub(":name", sanitize(author.name))
- link_to(author_html, user_path(author), class: "author_link has-tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe
+ link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe
end
end
@@ -144,6 +144,10 @@ module ProjectsHelper
nav_tabs << :settings
end
+ if can?(current_user, :read_project_member, project)
+ nav_tabs << :team
+ end
+
if can?(current_user, :read_issue, project)
nav_tabs << :issues
end
@@ -216,40 +220,14 @@ module ProjectsHelper
end
end
- def add_contribution_guide_path(project)
- if project && !project.repository.contribution_guide
- namespace_project_new_blob_path(
- project.namespace,
- project,
- project.default_branch,
- file_name: "CONTRIBUTING.md",
- commit_message: "Add contribution guide"
- )
- end
- end
-
- def add_changelog_path(project)
- if project && !project.repository.changelog
- namespace_project_new_blob_path(
- project.namespace,
- project,
- project.default_branch,
- file_name: "CHANGELOG",
- commit_message: "Add changelog"
- )
- end
- end
-
- def add_license_path(project)
- if project && !project.repository.license
- namespace_project_new_blob_path(
- project.namespace,
- project,
- project.default_branch,
- file_name: "LICENSE",
- commit_message: "Add license"
- )
- end
+ def add_special_file_path(project, file_name:, commit_message: nil)
+ namespace_project_new_blob_path(
+ project.namespace,
+ project,
+ project.default_branch || 'master',
+ file_name: file_name,
+ commit_message: commit_message || "Add #{file_name.downcase}"
+ )
end
def contribution_guide_path(project)
@@ -272,7 +250,7 @@ module ProjectsHelper
end
def license_path(project)
- filename_path(project, :license)
+ filename_path(project, :license_blob)
end
def version_path(project)
@@ -306,6 +284,13 @@ module ProjectsHelper
namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md')
end
+ def new_license_path
+ ref = @repository.root_ref if @repository
+ ref ||= 'master'
+
+ namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'LICENSE')
+ end
+
def last_push_event
if current_user
current_user.recent_push(@project.id)
@@ -335,6 +320,12 @@ module ProjectsHelper
@ref || @repository.try(:root_ref)
end
+ def license_short_name(project)
+ license = Licensee::License.new(project.repository.license_key)
+
+ license.nickname || license.name
+ end
+
private
def filename_path(project, filename)
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 4fc6de59a8b..e951a87a212 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -2,32 +2,29 @@ module SelectsHelper
def users_select_tag(id, opts = {})
css_class = "ajax-users-select "
css_class << "multiselect " if opts[:multiple]
+ css_class << "skip_ldap " if opts[:skip_ldap]
css_class << (opts[:class] || '')
value = opts[:selected] || ''
- placeholder = opts[:placeholder] || 'Search for a user'
- null_user = opts[:null_user] || false
- any_user = opts[:any_user] || false
- email_user = opts[:email_user] || false
- first_user = opts[:first_user] && current_user ? current_user.username : false
- current_user = opts[:current_user] || false
- author_id = opts[:author_id] || ''
- project = opts[:project] || @project
+ first_user = opts[:first_user] && current_user ? current_user.username : false
html = {
class: css_class,
data: {
- placeholder: placeholder,
- null_user: null_user,
- any_user: any_user,
- email_user: email_user,
+ placeholder: opts[:placeholder] || 'Search for a user',
+ null_user: opts[:null_user] || false,
+ any_user: opts[:any_user] || false,
+ email_user: opts[:email_user] || false,
first_user: first_user,
- current_user: current_user,
- author_id: author_id
+ current_user: opts[:current_user] || false,
+ "push-code-to-protected-branches" => opts[:push_code_to_protected_branches],
+ author_id: opts[:author_id] || ''
}
}
unless opts[:scope] == :all
+ project = opts[:project] || @project
+
if project
html['data-project-id'] = project.id
elsif @group
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 2f2d2721d6d..630e10ea892 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -8,6 +8,8 @@ module SortingHelper
sort_value_oldest_created => sort_title_oldest_created,
sort_value_milestone_soon => sort_title_milestone_soon,
sort_value_milestone_later => sort_title_milestone_later,
+ sort_value_due_date_soon => sort_title_due_date_soon,
+ sort_value_due_date_later => sort_title_due_date_later,
sort_value_largest_repo => sort_title_largest_repo,
sort_value_recently_signin => sort_title_recently_signin,
sort_value_oldest_signin => sort_title_oldest_signin,
@@ -50,6 +52,14 @@ module SortingHelper
'Milestone due later'
end
+ def sort_title_due_date_soon
+ 'Due soon'
+ end
+
+ def sort_title_due_date_later
+ 'Due later'
+ end
+
def sort_title_name
'Name'
end
@@ -98,6 +108,14 @@ module SortingHelper
'milestone_due_desc'
end
+ def sort_value_due_date_soon
+ 'due_date_asc'
+ end
+
+ def sort_value_due_date_later
+ 'due_date_desc'
+ end
+
def sort_value_name
'name_asc'
end
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 04e53fe7c61..96a83671009 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -110,4 +110,12 @@ module TabHelper
'active'
end
end
+
+ def profile_tab_class
+ if controller.controller_path =~ /\Aprofiles/
+ return 'active'
+ end
+
+ 'active' if current_controller?('oauth/applications')
+ end
end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 4920ca5af6e..dbedf417fa5 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -66,7 +66,7 @@ module TreeHelper
ref
else
project = tree_edit_project(project)
- project.repository.next_patch_branch
+ project.repository.next_branch('patch')
end
end
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 55bb4f65270..9dd11d20ea6 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -56,7 +56,7 @@ module Emails
{
from: sender(sender_id),
to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})")
+ subject: subject("#{@merge_request.title} (#{@merge_request.to_reference})")
}
end
end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index f9650df9a74..cdc40b81ee1 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -38,7 +38,7 @@ module Emails
{
from: sender(@note.author_id),
to: recipient(recipient_id),
- subject: subject("#{@note.noteable.title} (##{@note.noteable.iid})")
+ subject: subject("#{@note.noteable.title} (#{@note.noteable.to_reference})")
}
end
diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb
index 2bff5b63cc4..21db2fe04a0 100644
--- a/app/mailers/repository_check_mailer.rb
+++ b/app/mailers/repository_check_mailer.rb
@@ -8,7 +8,7 @@ class RepositoryCheckMailer < BaseMailer
mail(
to: User.admins.pluck(:email),
- subject: @message
+ subject: "GitLab Admin | #{@message}"
)
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 85ef0523b31..553cd447971 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -37,8 +37,6 @@
module Ci
class Build < CommitStatus
- LAZY_ATTRIBUTES = ['trace']
-
belongs_to :runner, class_name: 'Ci::Runner'
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
belongs_to :erased_by, class_name: 'User'
@@ -50,25 +48,17 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
- scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) }
mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader
acts_as_taggable
- # To prevent db load megabytes of data from trace
- default_scope -> { select(Ci::Build.columns_without_lazy) }
-
before_destroy { project }
- class << self
- def columns_without_lazy
- (column_names - LAZY_ATTRIBUTES).map do |column_name|
- "#{table_name}.#{column_name}"
- end
- end
+ after_create :execute_hooks
+ class << self
def last_month
where('created_at > ?', Date.today - 1.month)
end
@@ -126,12 +116,16 @@ module Ci
end
def retried?
- !self.commit.latest_statuses_for_ref(self.ref).include?(self)
+ !self.commit.statuses.latest.include?(self)
+ end
+
+ def retry
+ Ci::Build.retry(self)
end
def depends_on_builds
# Get builds of the same type
- latest_builds = self.commit.builds.similar(self).latest
+ latest_builds = self.commit.builds.latest
# Return builds from previous stages
latest_builds.where('stage_idx < ?', stage_idx)
@@ -230,12 +224,33 @@ module Ci
end
end
+ def trace_length
+ if raw_trace
+ raw_trace.length
+ else
+ 0
+ end
+ end
+
def trace=(trace)
+ recreate_trace_dir
+ File.write(path_to_trace, trace)
+ end
+
+ def recreate_trace_dir
unless Dir.exists?(dir_to_trace)
FileUtils.mkdir_p(dir_to_trace)
end
+ end
+ private :recreate_trace_dir
- File.write(path_to_trace, trace)
+ def append_trace(trace_part, offset)
+ recreate_trace_dir
+
+ File.truncate(path_to_trace, offset) if File.exist?(path_to_trace)
+ File.open(path_to_trace, 'a') do |f|
+ f.write(trace_part)
+ end
end
def dir_to_trace
diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb
index f4cf7034b14..f2667e5476b 100644
--- a/app/models/ci/commit.rb
+++ b/app/models/ci/commit.rb
@@ -19,21 +19,28 @@
module Ci
class Commit < ActiveRecord::Base
extend Ci::Model
+ include Statuseable
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
has_many :statuses, class_name: 'CommitStatus'
has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
+ delegate :stages, to: :statuses
+
validates_presence_of :sha
+ validates_presence_of :status
validate :valid_commit_sha
+ # Invalidate object and save if when touched
+ after_touch :update_state
+
def self.truncate_sha(sha)
sha[0...8]
end
- def to_param
- sha
+ def self.stages
+ CommitStatus.where(commit: all).stages
end
def project_id
@@ -68,15 +75,20 @@ module Ci
nil
end
- def stage
- running_or_pending = statuses.latest.running_or_pending.ordered
- running_or_pending.first.try(:stage)
+ def branch?
+ !tag?
+ end
+
+ def retryable?
+ builds.latest.any? do |build|
+ build.failed? && build.retryable?
+ end
end
- def create_builds(ref, tag, user, trigger_request = nil)
+ def create_builds(user, trigger_request = nil)
return unless config_processor
config_processor.stages.any? do |stage|
- CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request, 'success').present?
+ CreateBuildsService.new(self).execute(stage, user, 'success', trigger_request).present?
end
end
@@ -84,7 +96,7 @@ module Ci
return unless config_processor
# don't create other builds if this one is retried
- latest_builds = builds.similar(build).latest
+ latest_builds = builds.latest
return unless latest_builds.exists?(build.id)
# get list of stages after this build
@@ -92,88 +104,21 @@ module Ci
next_stages.delete(build.stage)
# get status for all prior builds
- prior_builds = latest_builds.reject { |other_build| next_stages.include?(other_build.stage) }
- status = Ci::Status.get_status(prior_builds)
+ prior_builds = latest_builds.where.not(stage: next_stages)
+ prior_status = prior_builds.status
# create builds for next stages based
next_stages.any? do |stage|
- CreateBuildsService.new.execute(self, stage, build.ref, build.tag, build.user, build.trigger_request, status).present?
+ CreateBuildsService.new(self).execute(stage, build.user, prior_status, build.trigger_request).present?
end
end
- def refs
- statuses.order(:ref).pluck(:ref).uniq
- end
-
- def latest_statuses
- @latest_statuses ||= statuses.latest.to_a
- end
-
- def latest_statuses_for_ref(ref)
- latest_statuses.select { |status| status.ref == ref }
- end
-
- def matrix_builds(build = nil)
- matrix_builds = builds.latest.ordered
- matrix_builds = matrix_builds.similar(build) if build
- matrix_builds.to_a
- end
-
def retried
@retried ||= (statuses.order(id: :desc) - statuses.latest)
end
- def status
- if yaml_errors.present?
- return 'failed'
- end
-
- @status ||= Ci::Status.get_status(latest_statuses)
- end
-
- def pending?
- status == 'pending'
- end
-
- def running?
- status == 'running'
- end
-
- def success?
- status == 'success'
- end
-
- def failed?
- status == 'failed'
- end
-
- def canceled?
- status == 'canceled'
- end
-
- def active?
- running? || pending?
- end
-
- def complete?
- canceled? || success? || failed?
- end
-
- def duration
- duration_array = statuses.map(&:duration).compact
- duration_array.reduce(:+).to_i
- end
-
- def started_at
- @started_at ||= statuses.order('started_at ASC').first.try(:started_at)
- end
-
- def finished_at
- @finished_at ||= statuses.order('finished_at DESC').first.try(:finished_at)
- end
-
def coverage
- coverage_array = latest_statuses.map(&:coverage).compact
+ coverage_array = statuses.latest.map(&:coverage).compact
if coverage_array.size >= 1
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end
@@ -181,23 +126,29 @@ module Ci
def config_processor
return nil unless ci_yaml_file
- @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
- rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
- save_yaml_error(e.message)
- nil
- rescue
- save_yaml_error("Undefined error")
- nil
+ return @config_processor if defined?(@config_processor)
+
+ @config_processor ||= begin
+ Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
+ rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
+ save_yaml_error(e.message)
+ nil
+ rescue
+ save_yaml_error("Undefined error")
+ nil
+ end
end
def ci_yaml_file
+ return @ci_yaml_file if defined?(@ci_yaml_file)
+
@ci_yaml_file ||= begin
blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
blob.load_all_data!(project.repository)
blob.data
+ rescue
+ nil
end
- rescue
- nil
end
def skip_ci?
@@ -206,10 +157,23 @@ module Ci
private
+ def update_state
+ statuses.reload
+ self.status = if yaml_errors.blank?
+ statuses.latest.status || 'skipped'
+ else
+ 'failed'
+ end
+ self.started_at = statuses.started_at
+ self.finished_at = statuses.finished_at
+ self.duration = statuses.latest.duration
+ save
+ end
+
def save_yaml_error(error)
return if self.yaml_errors?
self.yaml_errors = error
- save
+ update_state
end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index d1f07ccd55c..562c3ed15b2 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -15,8 +15,8 @@ class Commit
DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
# Commits above this size will not be rendered in HTML
- DIFF_HARD_LIMIT_FILES = 1000 unless defined?(DIFF_HARD_LIMIT_FILES)
- DIFF_HARD_LIMIT_LINES = 50000 unless defined?(DIFF_HARD_LIMIT_LINES)
+ DIFF_HARD_LIMIT_FILES = 1000
+ DIFF_HARD_LIMIT_LINES = 50000
class << self
def decorate(commits, project)
@@ -207,17 +207,22 @@ class Commit
@raw.short_id(7)
end
- def ci_commit
- project.ci_commit(sha)
+ def ci_commits
+ @ci_commits ||= project.ci_commits.where(sha: sha)
end
def status
- ci_commit.try(:status) || :not_found
+ return @status if defined?(@status)
+ @status ||= ci_commits.status
end
def revert_branch_name
"revert-#{short_id}"
end
+
+ def cherry_pick_branch_name
+ project.repository.next_branch("cherry-pick-#{short_id}", mild: true)
+ end
def revert_description
if merged_merge_request
@@ -253,6 +258,10 @@ class Commit
end.any? { |commit_ref| commit_ref.reverts_commit?(self) }
end
+ def change_type_title
+ merged_merge_request ? 'merge request' : 'commit'
+ end
+
private
def repo_changes
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 3377a85a55a..aa56314aa16 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -33,30 +33,23 @@
#
class CommitStatus < ActiveRecord::Base
+ include Statuseable
+
self.table_name = 'ci_builds'
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
- belongs_to :commit, class_name: 'Ci::Commit'
+ belongs_to :commit, class_name: 'Ci::Commit', touch: true
belongs_to :user
validates :commit, presence: true
- validates :status, inclusion: { in: %w(pending running failed success canceled) }
validates_presence_of :name
alias_attribute :author, :user
- scope :running, -> { where(status: 'running') }
- scope :pending, -> { where(status: 'pending') }
- scope :success, -> { where(status: 'success') }
- scope :failed, -> { where(status: 'failed') }
- scope :running_or_pending, -> { where(status: [:running, :pending]) }
- scope :finished, -> { where(status: [:success, :failed, :canceled]) }
- scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) }
+ scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) }
scope :ordered, -> { order(:ref, :stage_idx, :name) }
- scope :for_ref, ->(ref) { where(ref: ref) }
-
- AVAILABLE_STATUSES = ['pending', 'running', 'success', 'failed', 'canceled']
+ scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
state_machine :status, initial: :pending do
event :run do
@@ -86,31 +79,24 @@ class CommitStatus < ActiveRecord::Base
after_transition [:pending, :running] => :success do |commit_status|
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status)
end
-
- state :pending, value: 'pending'
- state :running, value: 'running'
- state :failed, value: 'failed'
- state :success, value: 'success'
- state :canceled, value: 'canceled'
end
- delegate :sha, :short_sha, to: :commit, prefix: false
+ delegate :sha, :short_sha, to: :commit
- # TODO: this should be removed with all references
def before_sha
- Gitlab::Git::BLANK_SHA
+ commit.before_sha || Gitlab::Git::BLANK_SHA
end
- def started?
- !pending? && !canceled? && started_at
+ def self.stages
+ order_by = 'max(stage_idx)'
+ group('stage').order(order_by).pluck(:stage, order_by).map(&:first).compact
end
- def active?
- running? || pending?
- end
-
- def complete?
- canceled? || success? || failed?
+ def self.stages_status
+ all.stages.inject({}) do |h, stage|
+ h[stage] = all.where(stage: stage).status
+ h
+ end
end
def ignored?
@@ -118,11 +104,13 @@ class CommitStatus < ActiveRecord::Base
end
def duration
- if started_at && finished_at
- finished_at - started_at
- elsif started_at
- Time.now - started_at
- end
+ duration =
+ if started_at && finished_at
+ finished_at - started_at
+ elsif started_at
+ Time.now - started_at
+ end
+ duration
end
def stuck?
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index afa2ca039ae..d5166e81474 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -37,7 +37,6 @@ module Issuable
scope :closed, -> { with_state(:closed) }
scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') }
scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') }
- scope :with_label, ->(title) { joins(:labels).where(labels: { title: title }) }
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :join_project, -> { joins(:project) }
@@ -122,6 +121,14 @@ module Issuable
joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC")
end
+
+ def with_label(title)
+ if title.is_a?(Array) && title.count > 1
+ joins(:labels).where(labels: { title: title }).group('issues.id').having("count(distinct labels.title) = #{title.count}")
+ else
+ joins(:labels).where(labels: { title: title })
+ end
+ end
end
def today?
diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb
new file mode 100644
index 00000000000..8a293b7b76e
--- /dev/null
+++ b/app/models/concerns/statuseable.rb
@@ -0,0 +1,81 @@
+module Statuseable
+ extend ActiveSupport::Concern
+
+ AVAILABLE_STATUSES = %w(pending running success failed canceled skipped)
+
+ class_methods do
+ def status_sql
+ builds = all.select('count(*)').to_sql
+ success = all.success.select('count(*)').to_sql
+ ignored = all.ignored.select('count(*)').to_sql if all.respond_to?(:ignored)
+ ignored ||= '0'
+ pending = all.pending.select('count(*)').to_sql
+ running = all.running.select('count(*)').to_sql
+ canceled = all.canceled.select('count(*)').to_sql
+ skipped = all.skipped.select('count(*)').to_sql
+
+ deduce_status = "(CASE
+ WHEN (#{builds})=0 THEN NULL
+ WHEN (#{builds})=(#{success})+(#{ignored}) THEN 'success'
+ WHEN (#{builds})=(#{pending}) THEN 'pending'
+ WHEN (#{builds})=(#{canceled}) THEN 'canceled'
+ WHEN (#{builds})=(#{skipped}) THEN 'skipped'
+ WHEN (#{running})+(#{pending})>0 THEN 'running'
+ ELSE 'failed'
+ END)"
+
+ deduce_status
+ end
+
+ def status
+ all.pluck(self.status_sql).first
+ end
+
+ def duration
+ duration_array = all.map(&:duration).compact
+ duration_array.reduce(:+)
+ end
+
+ def started_at
+ all.minimum(:started_at)
+ end
+
+ def finished_at
+ all.maximum(:finished_at)
+ end
+ end
+
+ included do
+ validates :status, inclusion: { in: AVAILABLE_STATUSES }
+
+ state_machine :status, initial: :pending do
+ state :pending, value: 'pending'
+ state :running, value: 'running'
+ state :failed, value: 'failed'
+ state :success, value: 'success'
+ state :canceled, value: 'canceled'
+ state :skipped, value: 'skipped'
+ end
+
+ scope :running, -> { where(status: 'running') }
+ scope :pending, -> { where(status: 'pending') }
+ scope :success, -> { where(status: 'success') }
+ scope :failed, -> { where(status: 'failed') }
+ scope :canceled, -> { where(status: 'canceled') }
+ scope :skipped, -> { where(status: 'skipped') }
+ scope :running_or_pending, -> { where(status: [:running, :pending]) }
+ scope :finished, -> { where(status: [:success, :failed, :canceled]) }
+ end
+
+ def started?
+ !pending? && !canceled? && started_at
+ end
+
+ def active?
+ running? || pending?
+ end
+
+ def complete?
+ canceled? || success? || failed?
+ end
+end
diff --git a/app/models/group.rb b/app/models/group.rb
index 9a04ac70d35..1f8432e3320 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -15,7 +15,6 @@
#
require 'carrierwave/orm/activerecord'
-require 'file_size_validator'
class Group < Namespace
include Gitlab::ConfigHelper
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index fe923fafbe0..bc6e0f98c3c 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -21,10 +21,9 @@
class ProjectHook < WebHook
belongs_to :project
- scope :push_hooks, -> { where(push_events: true) }
- scope :tag_push_hooks, -> { where(tag_push_events: true) }
scope :issue_hooks, -> { where(issues_events: true) }
scope :note_hooks, -> { where(note_events: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true) }
scope :build_hooks, -> { where(build_events: true) }
+ scope :wiki_page_hooks, -> { where(wiki_page_events: true) }
end
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index c147d8762a9..15dddcc2447 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -19,4 +19,7 @@
#
class SystemHook < WebHook
+ def async_execute(data, hook_name)
+ Sidekiq::Client.enqueue(SystemHookWorker, id, data, hook_name)
+ end
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 7a13c3f0a39..3a2e4f546f7 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -30,6 +30,9 @@ class WebHook < ActiveRecord::Base
default_value_for :build_events, false
default_value_for :enable_ssl_verification, true
+ scope :push_hooks, -> { where(push_events: true) }
+ scope :tag_push_hooks, -> { where(tag_push_events: true) }
+
# HTTParty timeout
default_timeout Gitlab.config.gitlab.webhook_timeout
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 3f188e04770..ea1bfb776ee 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -20,7 +20,6 @@
#
require 'carrierwave/orm/activerecord'
-require 'file_size_validator'
class Issue < ActiveRecord::Base
include InternalId
@@ -29,6 +28,13 @@ class Issue < ActiveRecord::Base
include Sortable
include Taskable
+ DueDateStruct = Struct.new(:title, :name).freeze
+ NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
+ AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze
+ Overdue = DueDateStruct.new('Overdue', 'overdue').freeze
+ DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze
+ DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze
+
ActsAsTaggableOn.strict_case_match = true
belongs_to :project
@@ -40,6 +46,13 @@ class Issue < ActiveRecord::Base
scope :open_for, ->(user) { opened.assigned_to(user) }
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
+ scope :without_due_date, -> { where(due_date: nil) }
+ scope :due_before, ->(date) { where('issues.due_date < ?', date) }
+ scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
+
+ scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
+ scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
+
state_machine :state, initial: :opened do
event :close do
transition [:reopened, :opened] => :closed
@@ -83,6 +96,15 @@ class Issue < ActiveRecord::Base
@link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
end
+ def self.sort(method)
+ case method.to_s
+ when 'due_date_asc' then order_due_date_asc
+ when 'due_date_desc' then order_due_date_desc
+ else
+ super
+ end
+ end
+
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
@@ -104,10 +126,16 @@ class Issue < ActiveRecord::Base
end
end
- def related_branches
- project.repository.branch_names.select do |branch|
+ # All branches containing the current issue's ID, except for
+ # those with a merge request open referencing the current issue.
+ def related_branches(current_user)
+ branches_with_iid = project.repository.branch_names.select do |branch|
branch =~ /\A#{iid}-(?!\d+-stable)/i
end
+
+ branches_with_merge_request = self.referenced_merge_requests(current_user).map(&:source_branch)
+
+ branches_with_iid - branches_with_merge_request
end
# Reset issue events cache
@@ -151,13 +179,21 @@ class Issue < ActiveRecord::Base
end
def to_branch_name
- "#{iid}-#{title.parameterize}"
+ if self.confidential?
+ "#{iid}-confidential-issue"
+ else
+ "#{iid}-#{title.parameterize}"
+ end
end
def can_be_worked_on?(current_user)
!self.closed? &&
!self.project.forked? &&
- self.related_branches.empty? &&
+ self.related_branches(current_user).empty? &&
self.closed_by_merge_requests(current_user).empty?
end
+
+ def overdue?
+ due_date.try(:past?) || false
+ end
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 55c01cae762..60bdce32952 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -113,6 +113,10 @@ class Label < ActiveRecord::Base
template
end
+ def text_color
+ LabelsHelper::text_color_for_bg(self.color)
+ end
+
private
def label_format_reference(format = :id)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index e410febdfff..d00919c3b0c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -27,9 +27,6 @@
# merge_commit_sha :string
#
-require Rails.root.join("app/models/commit")
-require Rails.root.join("lib/static_model")
-
class MergeRequest < ActiveRecord::Base
include InternalId
include Issuable
@@ -589,7 +586,7 @@ class MergeRequest < ActiveRecord::Base
end
def ci_commit
- @ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project
+ @ci_commit ||= source_project.ci_commit(last_commit.id, source_branch) if last_commit && source_project
end
def diff_refs
@@ -605,4 +602,8 @@ class MergeRequest < ActiveRecord::Base
def can_be_reverted?(current_user = nil)
merge_commit && !merge_commit.has_been_reverted?(current_user, self)
end
+
+ def can_be_cherry_picked?
+ merge_commit
+ end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 33884118595..0580cafdd1b 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -11,8 +11,6 @@
# updated_at :datetime
#
-require Rails.root.join("app/models/commit")
-
class MergeRequestDiff < ActiveRecord::Base
include Sortable
diff --git a/app/models/note.rb b/app/models/note.rb
index 87ced65c650..71b4293d57a 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -20,7 +20,6 @@
#
require 'carrierwave/orm/activerecord'
-require 'file_size_validator'
class Note < ActiveRecord::Base
include Gitlab::CurrentSettings
diff --git a/app/models/project.rb b/app/models/project.rb
index 8f20922e3c5..7aa21b19e67 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -40,7 +40,6 @@
#
require 'carrierwave/orm/activerecord'
-require 'file_size_validator'
class Project < ActiveRecord::Base
include Gitlab::ConfigHelper
@@ -831,8 +830,8 @@ class Project < ActiveRecord::Base
end
end
- def hook_attrs
- {
+ def hook_attrs(backward: true)
+ attrs = {
name: name,
description: description,
web_url: web_url,
@@ -843,12 +842,19 @@ class Project < ActiveRecord::Base
visibility_level: visibility_level,
path_with_namespace: path_with_namespace,
default_branch: default_branch,
- # Backward compatibility
- homepage: web_url,
- url: url_to_repo,
- ssh_url: ssh_url_to_repo,
- http_url: http_url_to_repo
}
+
+ # Backward compatibility
+ if backward
+ attrs.merge!({
+ homepage: web_url,
+ url: url_to_repo,
+ ssh_url: ssh_url_to_repo,
+ http_url: http_url_to_repo
+ })
+ end
+
+ attrs
end
# Reset events cache related to this project
@@ -957,12 +963,12 @@ class Project < ActiveRecord::Base
!namespace.share_with_group_lock
end
- def ci_commit(sha)
- ci_commits.find_by(sha: sha)
+ def ci_commit(sha, ref)
+ ci_commits.order(id: :desc).find_by(sha: sha, ref: ref)
end
- def ensure_ci_commit(sha)
- ci_commit(sha) || ci_commits.create(sha: sha)
+ def ensure_ci_commit(sha, ref)
+ ci_commit(sha, ref) || ci_commits.create(sha: sha, ref: ref)
end
def enable_ci
diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb
index 79efb403058..2c0ae312f1b 100644
--- a/app/models/project_import_data.rb
+++ b/app/models/project_import_data.rb
@@ -8,7 +8,6 @@
#
require 'carrierwave/orm/activerecord'
-require 'file_size_validator'
class ProjectImportData < ActiveRecord::Base
belongs_to :project
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 0e3fa4a40fe..064ef8e8674 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -183,7 +183,7 @@ class HipchatService < Service
title = obj_attr[:title]
merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}"
- merge_request_link = "<a href=\"#{merge_request_url}\">merge request ##{merge_request_id}</a>"
+ merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>"
message = "#{user_name} #{state} #{merge_request_link} in " \
"#{project_link}: <b>#{title}</b>"
@@ -224,7 +224,7 @@ class HipchatService < Service
when "MergeRequest"
subj_attr = HashWithIndifferentAccess.new(data[:merge_request])
subject_id = subj_attr[:iid]
- subject_desc = "##{subject_id}"
+ subject_desc = "!#{subject_id}"
subject_type = "merge request"
title = format_title(subj_attr[:title])
when "Snippet"
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index d89cf6d17b2..fd65027f084 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -60,7 +60,7 @@ class SlackService < Service
end
def supported_events
- %w(push issue merge_request note tag_push build)
+ %w(push issue merge_request note tag_push build wiki_page)
end
def execute(data)
@@ -90,6 +90,8 @@ class SlackService < Service
NoteMessage.new(data)
when "build"
BuildMessage.new(data) if should_build_be_notified?(data)
+ when "wiki_page"
+ WikiPageMessage.new(data)
end
opt = {}
@@ -133,3 +135,4 @@ require "slack_service/push_message"
require "slack_service/merge_message"
require "slack_service/note_message"
require "slack_service/build_message"
+require "slack_service/wiki_page_message"
diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb
index e792c258f73..11fc691022b 100644
--- a/app/models/project_services/slack_service/merge_message.rb
+++ b/app/models/project_services/slack_service/merge_message.rb
@@ -50,7 +50,7 @@ class SlackService
end
def merge_request_link
- "[merge request ##{merge_request_id}](#{merge_request_url})"
+ "[merge request !#{merge_request_id}](#{merge_request_url})"
end
def merge_request_url
diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb
index b15d9a14677..89ba51cb662 100644
--- a/app/models/project_services/slack_service/note_message.rb
+++ b/app/models/project_services/slack_service/note_message.rb
@@ -58,7 +58,7 @@ class SlackService
def create_merge_note(merge_request)
commented_on_message(
- "[merge request ##{merge_request[:iid]}](#{@note_url})",
+ "[merge request !#{merge_request[:iid]}](#{@note_url})",
format_title(merge_request[:title]))
end
diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/slack_service/wiki_page_message.rb
new file mode 100644
index 00000000000..f336d9e7691
--- /dev/null
+++ b/app/models/project_services/slack_service/wiki_page_message.rb
@@ -0,0 +1,53 @@
+class SlackService
+ class WikiPageMessage < BaseMessage
+ attr_reader :user_name
+ attr_reader :title
+ attr_reader :project_name
+ attr_reader :project_url
+ attr_reader :wiki_page_url
+ attr_reader :action
+ attr_reader :description
+
+ def initialize(params)
+ @user_name = params[:user][:name]
+ @project_name = params[:project_name]
+ @project_url = params[:project_url]
+
+ obj_attr = params[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ @title = obj_attr[:title]
+ @wiki_page_url = obj_attr[:url]
+ @description = obj_attr[:content]
+
+ @action =
+ case obj_attr[:action]
+ when "create"
+ "created"
+ when "update"
+ "edited"
+ end
+ end
+
+ def attachments
+ description_message
+ end
+
+ private
+
+ def message
+ "#{user_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
+ end
+
+ def description_message
+ [{ text: format(@description), color: attachment_color }]
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def wiki_page_link
+ "[wiki page](#{wiki_page_url})"
+ end
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 589756f8531..da751591103 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -228,7 +228,8 @@ class Repository
def cache_keys
%i(size branch_names tag_names commit_count
- readme version contribution_guide changelog license)
+ readme version contribution_guide changelog
+ license_blob license_key)
end
def build_cache
@@ -461,27 +462,21 @@ class Repository
end
end
- def license
- cache.fetch(:license) do
- licenses = tree(:head).blobs.find_all do |file|
- file.name =~ /\A(copying|license|licence)/i
- end
-
- preferences = [
- /\Alicen[sc]e\z/i, # LICENSE, LICENCE
- /\Alicen[sc]e\./i, # LICENSE.md, LICENSE.txt
- /\Acopying\z/i, # COPYING
- /\Acopying\.(?!lesser)/i, # COPYING.txt
- /Acopying.lesser/i # COPYING.LESSER
- ]
+ def license_blob
+ return nil if !exists? || empty?
- license = nil
- preferences.each do |r|
- license = licenses.find { |l| l.name =~ r }
- break if license
+ cache.fetch(:license_blob) do
+ if licensee_project.license
+ blob_at_branch(root_ref, licensee_project.matched_file.filename)
end
+ end
+ end
- license
+ def license_key
+ return nil if !exists? || empty?
+
+ cache.fetch(:license_key) do
+ licensee_project.license.try(:key) || 'no-license'
end
end
@@ -549,15 +544,18 @@ class Repository
commit(sha)
end
- def next_patch_branch
- patch_branch_ids = self.branch_names.map do |n|
- result = n.match(/\Apatch-([0-9]+)\z/)
+ def next_branch(name, opts={})
+ branch_ids = self.branch_names.map do |n|
+ next 1 if n == name
+ result = n.match(/\A#{name}-([0-9]+)\z/)
result[1].to_i if result
end.compact
- highest_patch_branch_id = patch_branch_ids.max || 0
+ highest_branch_id = branch_ids.max || 0
- "patch-#{highest_patch_branch_id + 1}"
+ return name if opts[:mild] && 0 == highest_branch_id
+
+ "#{name}-#{highest_branch_id + 1}"
end
# Remove archives older than 2 hours
@@ -760,6 +758,28 @@ class Repository
end
end
+ def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil)
+ source_sha = find_branch(base_branch).target
+ cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch)
+
+ return false unless cherry_pick_tree_id
+
+ commit_with_hooks(user, base_branch) do |ref|
+ committer = user_to_committer(user)
+ source_sha = Rugged::Commit.create(rugged,
+ message: commit.message,
+ author: {
+ email: commit.author_email,
+ name: commit.author_name,
+ time: commit.authored_date
+ },
+ committer: committer,
+ tree: cherry_pick_tree_id,
+ parents: [rugged.lookup(source_sha)],
+ update_ref: ref)
+ end
+ end
+
def check_revert_content(commit, base_branch)
source_sha = find_branch(base_branch).target
args = [commit.id, source_sha]
@@ -774,6 +794,20 @@ class Repository
tree_id
end
+ def check_cherry_pick_content(commit, base_branch)
+ source_sha = find_branch(base_branch).target
+ args = [commit.id, source_sha]
+ args << 1 if commit.merge_commit?
+
+ cherry_pick_index = rugged.cherrypick_commit(*args)
+ return false if cherry_pick_index.conflicts?
+
+ tree_id = cherry_pick_index.write_tree(rugged)
+ return false unless diff_exists?(source_sha, tree_id)
+
+ tree_id
+ end
+
def diff_exists?(sha1, sha2)
rugged.diff(sha1, sha2).size > 0
end
@@ -925,4 +959,8 @@ class Repository
def cache
@cache ||= RepositoryCache.new(path_with_namespace)
end
+
+ def licensee_project
+ @licensee_project ||= Licensee.project(path)
+ end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 721273250ea..2645b8321d7 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -32,6 +32,7 @@ class Service < ActiveRecord::Base
default_value_for :tag_push_events, true
default_value_for :note_events, true
default_value_for :build_events, true
+ default_value_for :wiki_page_events, true
after_initialize :initialize_properties
@@ -53,6 +54,7 @@ class Service < ActiveRecord::Base
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :build_hooks, -> { where(build_events: true, active: true) }
+ scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
default_value_for :category, 'common'
@@ -94,7 +96,7 @@ class Service < ActiveRecord::Base
end
def supported_events
- %w(push tag_push issue merge_request)
+ %w(push tag_push issue merge_request wiki_page)
end
def execute(data)
diff --git a/app/models/user.rb b/app/models/user.rb
index 031315debd7..ab48f8f1960 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -63,7 +63,6 @@
#
require 'carrierwave/orm/activerecord'
-require 'file_size_validator'
class User < ActiveRecord::Base
extend Gitlab::ConfigHelper
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 526760779a4..3d5fd9d3ee9 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -29,6 +29,10 @@ class WikiPage
# new Page values before writing to the Gollum repository.
attr_accessor :attributes
+ def hook_attrs
+ attributes
+ end
+
def initialize(wiki, page = nil, persisted = false)
@wiki = wiki
@page = page
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
index 2cd51a7610f..18274ce24e2 100644
--- a/app/services/ci/create_builds_service.rb
+++ b/app/services/ci/create_builds_service.rb
@@ -1,7 +1,11 @@
module Ci
class CreateBuildsService
- def execute(commit, stage, ref, tag, user, trigger_request, status)
- builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag, trigger_request)
+ def initialize(commit)
+ @commit = commit
+ end
+
+ def execute(stage, user, status, trigger_request = nil)
+ builds_attrs = config_processor.builds_for_stage_and_ref(stage, @commit.ref, @commit.tag, trigger_request)
# check when to create next build
builds_attrs = builds_attrs.select do |build_attrs|
@@ -17,7 +21,8 @@ module Ci
builds_attrs.map do |build_attrs|
# don't create the same build twice
- unless commit.builds.find_by(ref: ref, tag: tag, trigger_request: trigger_request, name: build_attrs[:name])
+ unless @commit.builds.find_by(ref: @commit.ref, tag: @commit.tag,
+ trigger_request: trigger_request, name: build_attrs[:name])
build_attrs.slice!(:name,
:commands,
:tag_list,
@@ -26,17 +31,21 @@ module Ci
:stage,
:stage_idx)
- build_attrs.merge!(ref: ref,
- tag: tag,
+ build_attrs.merge!(ref: @commit.ref,
+ tag: @commit.tag,
trigger_request: trigger_request,
user: user,
- project: commit.project)
+ project: @commit.project)
- build = commit.builds.create!(build_attrs)
- build.execute_hooks
- build
+ @commit.builds.create!(build_attrs)
end
end
end
+
+ private
+
+ def config_processor
+ @config_processor ||= @commit.config_processor
+ end
end
end
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
index b3dfc707221..993acf11db9 100644
--- a/app/services/ci/create_trigger_request_service.rb
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -7,14 +7,14 @@ module Ci
# check if ref is tag
tag = project.repository.find_tag(ref).present?
- ci_commit = project.ensure_ci_commit(commit.sha)
+ ci_commit = project.ci_commits.create(sha: commit.sha, ref: ref, tag: tag)
trigger_request = trigger.trigger_requests.create!(
variables: variables,
commit: ci_commit,
)
- if ci_commit.create_builds(ref, tag, nil, trigger_request)
+ if ci_commit.create_builds(nil, trigger_request)
trigger_request
end
end
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index 50c95ced8a7..3018f27ec05 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -3,8 +3,9 @@ module Ci
def execute(project, opts)
sha = opts[:sha] || ref_sha(project, opts[:ref])
- commit = project.ci_commits.find_by(sha: sha)
- image_name = image_for_commit(commit)
+ ci_commits = project.ci_commits.where(sha: sha)
+ ci_commits = ci_commits.where(ref: opts[:ref]) if opts[:ref]
+ image_name = image_for_status(ci_commits.status)
image_path = Rails.root.join('public/ci', image_name)
OpenStruct.new(path: image_path, name: image_name)
@@ -16,9 +17,9 @@ module Ci
project.commit(ref).try(:sha) if ref
end
- def image_for_commit(commit)
- return 'build-unknown.svg' unless commit
- 'build-' + commit.status + ".svg"
+ def image_for_status(status)
+ status ||= 'unknown'
+ 'build-' + status + ".svg"
end
end
end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
new file mode 100644
index 00000000000..6b69cb53b2c
--- /dev/null
+++ b/app/services/commits/change_service.rb
@@ -0,0 +1,47 @@
+module Commits
+ class ChangeService < ::BaseService
+ class ValidationError < StandardError; end
+ class ChangeError < StandardError; end
+
+ def execute
+ @source_project = params[:source_project] || @project
+ @target_branch = params[:target_branch]
+ @commit = params[:commit]
+ @create_merge_request = params[:create_merge_request].present?
+
+ check_push_permissions unless @create_merge_request
+ commit
+ rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
+ ValidationError, ChangeError => ex
+ error(ex.message)
+ end
+
+ def commit
+ raise NotImplementedError
+ end
+
+ private
+
+ def check_push_permissions
+ allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
+
+ unless allowed
+ raise ValidationError.new('You are not allowed to push into this branch')
+ end
+
+ true
+ end
+
+ def create_target_branch(new_branch)
+ # Temporary branch exists and contains the change commit
+ return success if repository.find_branch(new_branch)
+
+ result = CreateBranchService.new(@project, current_user)
+ .execute(new_branch, @target_branch, source_project: @source_project)
+
+ if result[:status] == :error
+ raise ChangeError, "There was an error creating the source branch: #{result[:message]}"
+ end
+ end
+ end
+end
diff --git a/app/services/commits/cherry_pick_service.rb b/app/services/commits/cherry_pick_service.rb
new file mode 100644
index 00000000000..f9a4efa7182
--- /dev/null
+++ b/app/services/commits/cherry_pick_service.rb
@@ -0,0 +1,19 @@
+module Commits
+ class CherryPickService < ChangeService
+ def commit
+ cherry_pick_into = @create_merge_request ? @commit.cherry_pick_branch_name : @target_branch
+ cherry_pick_tree_id = repository.check_cherry_pick_content(@commit, @target_branch)
+
+ if cherry_pick_tree_id
+ create_target_branch(cherry_pick_into) if @create_merge_request
+
+ repository.cherry_pick(current_user, @commit, cherry_pick_into, cherry_pick_tree_id)
+ success
+ else
+ error_msg = "Sorry, we cannot cherry-pick this #{@commit.change_type_title} automatically.
+ It may have already been cherry-picked, or a more recent commit may have updated some of its content."
+ raise ChangeError, error_msg
+ end
+ end
+ end
+end
diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb
index a3c950ede1f..c7de9f6f35e 100644
--- a/app/services/commits/revert_service.rb
+++ b/app/services/commits/revert_service.rb
@@ -1,21 +1,5 @@
module Commits
- class RevertService < ::BaseService
- class ValidationError < StandardError; end
- class ReversionError < StandardError; end
-
- def execute
- @source_project = params[:source_project] || @project
- @target_branch = params[:target_branch]
- @commit = params[:commit]
- @create_merge_request = params[:create_merge_request].present?
-
- check_push_permissions unless @create_merge_request
- commit
- rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
- ValidationError, ReversionError => ex
- error(ex.message)
- end
-
+ class RevertService < ChangeService
def commit
revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch
revert_tree_id = repository.check_revert_content(@commit, @target_branch)
@@ -26,34 +10,10 @@ module Commits
repository.revert(current_user, @commit, revert_into, revert_tree_id)
success
else
- error_msg = "Sorry, we cannot revert this #{params[:revert_type_title]} automatically.
+ error_msg = "Sorry, we cannot revert this #{@commit.change_type_title} automatically.
It may have already been reverted, or a more recent commit may have updated some of its content."
- raise ReversionError, error_msg
+ raise ChangeError, error_msg
end
end
-
- private
-
- def create_target_branch(new_branch)
- # Temporary branch exists and contains the revert commit
- return success if repository.find_branch(new_branch)
-
- result = CreateBranchService.new(@project, current_user)
- .execute(new_branch, @target_branch, source_project: @source_project)
-
- if result[:status] == :error
- raise ReversionError, "There was an error creating the source branch: #{result[:message]}"
- end
- end
-
- def check_push_permissions
- allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
-
- unless allowed
- raise ValidationError.new('You are not allowed to push into this branch')
- end
-
- true
- end
end
end
diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb
index 69d5c42a877..0d2aa1ff03d 100644
--- a/app/services/create_commit_builds_service.rb
+++ b/app/services/create_commit_builds_service.rb
@@ -2,6 +2,7 @@ class CreateCommitBuildsService
def execute(project, user, params)
return false unless project.builds_enabled?
+ before_sha = params[:checkout_sha] || params[:before]
sha = params[:checkout_sha] || params[:after]
origin_ref = params[:ref]
@@ -10,15 +11,16 @@ class CreateCommitBuildsService
end
ref = Gitlab::Git.ref_name(origin_ref)
+ tag = Gitlab::Git.tag_ref?(origin_ref)
# Skip branch removal
if sha == Gitlab::Git::BLANK_SHA
return false
end
- commit = project.ci_commit(sha)
+ commit = project.ci_commit(sha, ref)
unless commit
- commit = project.ci_commits.new(sha: sha)
+ commit = project.ci_commits.new(sha: sha, ref: ref, before_sha: before_sha, tag: tag)
# Skip creating ci_commit when no gitlab-ci.yml is found
unless commit.ci_yaml_file
@@ -32,10 +34,10 @@ class CreateCommitBuildsService
# Skip creating builds for commits that have [ci skip]
unless commit.skip_ci?
# Create builds for commit
- tag = Gitlab::Git.tag_ref?(origin_ref)
- commit.create_builds(ref, tag, user)
+ commit.create_builds(user)
end
+ commit.touch
commit
end
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index dc74c02760b..1e1be8cd04b 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -73,6 +73,7 @@ class GitPushService < BaseService
@project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user)
EventCreateService.new.push(@project, current_user, build_push_data)
+ SystemHooksService.new.execute_hooks(build_push_data_system_hook.dup, :push_hooks)
@project.execute_hooks(build_push_data.dup, :push_hooks)
@project.execute_services(build_push_data.dup, :push_hooks)
CreateCommitBuildsService.new.execute(@project, current_user, build_push_data)
@@ -138,6 +139,11 @@ class GitPushService < BaseService
build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits)
end
+ def build_push_data_system_hook
+ @push_data_system ||= Gitlab::PushDataBuilder.
+ build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], [])
+ end
+
def push_to_existing_branch?
# Return if this is not a push to a branch (e.g. new commits)
Gitlab::Git.branch_ref?(params[:ref]) && !Gitlab::Git.blank_ref?(params[:oldrev])
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index c88c7672805..64271d8bc5c 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -1,16 +1,16 @@
-class GitTagPushService
- attr_accessor :project, :user, :push_data
+class GitTagPushService < BaseService
+ attr_accessor :push_data
- def execute(project, user, oldrev, newrev, ref)
+ def execute
project.repository.before_push_tag
- @project, @user = project, user
- @push_data = build_push_data(oldrev, newrev, ref)
+ @push_data = build_push_data
- EventCreateService.new.push(project, user, @push_data)
+ EventCreateService.new.push(project, current_user, @push_data)
+ SystemHooksService.new.execute_hooks(build_system_push_data.dup, :tag_push_hooks)
project.execute_hooks(@push_data.dup, :tag_push_hooks)
project.execute_services(@push_data.dup, :tag_push_hooks)
- CreateCommitBuildsService.new.execute(project, @user, @push_data)
+ CreateCommitBuildsService.new.execute(project, current_user, @push_data)
ProjectCacheWorker.perform_async(project.id)
true
@@ -18,14 +18,14 @@ class GitTagPushService
private
- def build_push_data(oldrev, newrev, ref)
+ def build_push_data
commits = []
message = nil
- if !Gitlab::Git.blank_ref?(newrev)
- tag_name = Gitlab::Git.ref_name(ref)
+ if !Gitlab::Git.blank_ref?(params[:newrev])
+ tag_name = Gitlab::Git.ref_name(params[:ref])
tag = project.repository.find_tag(tag_name)
- if tag && tag.target == newrev
+ if tag && tag.target == params[:newrev]
commit = project.commit(tag.target)
commits = [commit].compact
message = tag.message
@@ -33,6 +33,11 @@ class GitTagPushService
end
Gitlab::PushDataBuilder.
- build(project, user, oldrev, newrev, ref, commits, message)
+ build(project, current_user, params[:oldrev], params[:newrev], params[:ref], commits, message)
+ end
+
+ def build_system_push_data
+ Gitlab::PushDataBuilder.
+ build(project, current_user, params[:oldrev], params[:newrev], params[:ref], [], '')
end
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index f0615ec7420..e43b5b51e5b 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -3,17 +3,13 @@ class SystemHooksService
execute_hooks(build_event_data(model, event))
end
- private
-
- def execute_hooks(data)
- SystemHook.all.each do |sh|
- async_execute_hook(sh, data, 'system_hooks')
+ def execute_hooks(data, hooks_scope = :all)
+ SystemHook.send(hooks_scope).each do |hook|
+ hook.async_execute(data, 'system_hooks')
end
end
- def async_execute_hook(hook, data, hook_name)
- Sidekiq::Client.enqueue(SystemHookWorker, hook.id, data, hook_name)
- end
+ private
def build_event_data(model, event)
data = {
diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb
new file mode 100644
index 00000000000..9162f128602
--- /dev/null
+++ b/app/services/wiki_pages/base_service.rb
@@ -0,0 +1,27 @@
+module WikiPages
+ class BaseService < ::BaseService
+
+ def hook_data(page, action)
+ hook_data = {
+ object_kind: page.class.name.underscore,
+ user: current_user.hook_attrs,
+ project: @project.hook_attrs,
+ object_attributes: page.hook_attrs,
+ # DEPRECATED
+ repository: @project.hook_attrs.slice(:name, :url, :description, :homepage)
+ }
+
+ page_url = Gitlab::UrlBuilder.build(page)
+ hook_data[:object_attributes].merge!(url: page_url, action: action)
+ hook_data
+ end
+
+ private
+
+ def execute_hooks(page, action = 'create')
+ page_data = hook_data(page, action)
+ @project.execute_hooks(page_data, :wiki_page_hooks)
+ @project.execute_services(page_data, :wiki_page_hooks)
+ end
+ end
+end
diff --git a/app/services/wiki_pages/create_service.rb b/app/services/wiki_pages/create_service.rb
new file mode 100644
index 00000000000..988c663b9d0
--- /dev/null
+++ b/app/services/wiki_pages/create_service.rb
@@ -0,0 +1,13 @@
+module WikiPages
+ class CreateService < WikiPages::BaseService
+ def execute
+ page = WikiPage.new(@project.wiki)
+
+ if page.create(@params)
+ execute_hooks(page, 'create')
+ end
+
+ page
+ end
+ end
+end
diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb
new file mode 100644
index 00000000000..8f6a50da838
--- /dev/null
+++ b/app/services/wiki_pages/update_service.rb
@@ -0,0 +1,11 @@
+module WikiPages
+ class UpdateService < WikiPages::BaseService
+ def execute(page)
+ if page.update(@params[:content], @params[:format], @params[:message])
+ execute_hooks(page, 'update')
+ end
+
+ page
+ end
+ end
+end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index aadd2c54f20..e0d8d16a954 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -155,7 +155,11 @@
= f.label :shared_runners_enabled do
= f.check_box :shared_runners_enabled
Enable shared runners for new projects
-
+ .form-group
+ = f.label :shared_runners_text, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :shared_runners_text, class: 'form-control', rows: 4
+ .help-block Markdown enabled
.form-group
= f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2'
.col-sm-10
@@ -214,6 +218,13 @@
.help-block
The sampling interval in seconds. Sampled data includes memory usage,
retained Ruby objects, file descriptors and so on.
+ .form-group
+ = f.label :metrics_packet_size, 'Metrics per packet', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_packet_size, class: 'form-control'
+ .help-block
+ The amount of points to store in a single UDP packet. More points
+ results in fewer but larger UDP packets being sent.
%fieldset
%legend Spam and Anti-bot Protection
@@ -282,7 +293,7 @@
= f.check_box :repository_checks_enabled
Enable Repository Checks
.help-block
- GitLab will periodically run
+ GitLab will periodically run
%a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck'
in all project and wiki repositories to look for silent disk corruption issues.
.form-group
@@ -290,7 +301,7 @@
= link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
.help-block
If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
-
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index ad952052f25..67d23c80233 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -17,6 +17,27 @@
.col-sm-10
= f.text_field :url, class: "form-control"
.form-group
+ = f.label :url, "Trigger", class: 'control-label'
+ .col-sm-10.prepend-top-10
+ %div
+ System hook will be triggered on set of events like creating project
+ or adding ssh key. But you can also enable extra triggers like Push events.
+
+ %div.prepend-top-default
+ = f.check_box :push_events, class: 'pull-left'
+ .prepend-left-20
+ = f.label :push_events, class: 'list-label' do
+ %strong Push events
+ %p.light
+ This url will be triggered by a push to the repository
+ %div
+ = f.check_box :tag_push_events, class: 'pull-left'
+ .prepend-left-20
+ = f.label :tag_push_events, class: 'list-label' do
+ %strong Tag push events
+ %p.light
+ This url will be triggered when a new tag is pushed to the repository
+ .form-group
= f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox'
.col-sm-10
.checkbox
@@ -31,13 +52,16 @@
.panel.panel-default
.panel-heading
System hooks (#{@hooks.count})
- %ul.well-list
+ %ul.content-list
- @hooks.each do |hook|
%li
- .list-item-name
- %strong= hook.url
- %p SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
-
- .pull-right
+ .controls
= link_to 'Test Hook', admin_hook_test_path(hook), class: "btn btn-sm"
= link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm"
+ .monospace= hook.url
+ %div
+ - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger|
+ - if hook.send(trigger)
+ %span.label.label-gray= trigger.titleize
+ %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
+
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index b05fdbd5552..fe0b9d3a491 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -7,17 +7,17 @@
.form-group
= f.label :name, class: 'control-label'
.col-sm-10
- = f.text_field :name, required: true, autocomplete: "off", class: 'form-control'
+ = f.text_field :name, required: true, autocomplete: 'off', class: 'form-control'
%span.help-inline * required
.form-group
= f.label :username, class: 'control-label'
.col-sm-10
- = f.text_field :username, required: true, autocomplete: "off", class: 'form-control'
+ = f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control'
%span.help-inline * required
.form-group
= f.label :email, class: 'control-label'
.col-sm-10
- = f.text_field :email, required: true, autocomplete: "off", class: 'form-control'
+ = f.text_field :email, required: true, autocomplete: 'off', class: 'form-control'
%span.help-inline * required
- if @user.new_record?
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index f9ec3a89158..49ab8aad1d5 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -45,6 +45,7 @@
.prepend-top-default
- if @todos.any?
+ .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
- @todos.group_by(&:project).each do |group|
.panel.panel-default.panel-small.js-todos-list
- project = group[0]
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index 0aff79749ef..79df17ba612 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -1,5 +1,4 @@
- page_title "Applications"
-- header_title page_title, applications_profile_path
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 9639da4cb58..5b7f11440c1 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -26,7 +26,7 @@
- @already_added_projects.each do |project|
%tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
%td
- = link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank"
+ = github_project_link(project.import_source)
%td
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
@@ -43,7 +43,7 @@
- @repos.each do |repo|
%tr{id: "repo_#{repo.id}"}
%td
- = link_to repo.full_name, "https://github.com/#{repo.full_name}", target: "_blank"
+ = github_project_link(repo.full_name)
%td.import-target
= repo.full_name
%td.import-actions.job-status
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index c799e9c588d..ca9c2a0bf2e 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -25,6 +25,10 @@
.content-wrapper
= render "layouts/flash"
= yield :flash_message
+ - if defined?(nav) && nav
+ .layout-nav
+ %div{ class: container_class }
+ = render "layouts/nav/#{nav}"
%div{ class: (container_class unless @no_container) }
.content
.clearfix
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index babfb032236..e4d1c773d03 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -6,6 +6,6 @@
= yield :scripts_body_top
= render "layouts/header/default", title: header_title
- = render 'layouts/page', sidebar: sidebar
+ = render 'layouts/page', sidebar: sidebar, nav: nav
= yield :scripts_body
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 5cef652da14..ca49c313ff7 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -48,8 +48,7 @@
%span
Help
- %li.separate-item
- = nav_link(controller: :profile) do
+ = nav_link(html_options: {class: profile_tab_class}) do
= link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
= icon('user fw')
%span
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 3b9d31a6fc5..d730840d63a 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -1,17 +1,9 @@
-%ul.nav.nav-sidebar
- = nav_link do
- = link_to root_path, title: 'Go to dashboard', class: 'back-link' do
- = icon('caret-square-o-left fw')
- %span
- Go to dashboard
-
- %li.separate-item
-
+%ul.nav-links
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path, title: 'Profile Settings' do
= icon('user fw')
%span
- Profile Settings
+ Profile
= nav_link(controller: [:accounts, :two_factor_auths]) do
= link_to profile_account_path, title: 'Account' do
= icon('gear fw')
@@ -27,7 +19,6 @@
= icon('envelope-o fw')
%span
Emails
- %span.count= number_with_delimiter(current_user.emails.count + 1)
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
= link_to edit_profile_password_path, title: 'Password' do
@@ -45,7 +36,6 @@
= icon('key fw')
%span
SSH Keys
- %span.count= number_with_delimiter(current_user.keys.count)
= nav_link(controller: :preferences) do
= link_to profile_preferences_path, title: 'Preferences' do
-# TODO (rspeicher): Better icon?
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 86b46e8c75e..a15b7758c4b 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -77,7 +77,7 @@
Merge Requests
%span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count)
- - if project_nav_tab? :settings
+ - if project_nav_tab? :team
= nav_link(controller: [:project_members, :teams]) do
= link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
= icon('users fw')
diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml
index dfa6cc5702e..b77d3402a2e 100644
--- a/app/views/layouts/profile.html.haml
+++ b/app/views/layouts/profile.html.haml
@@ -1,5 +1,6 @@
- page_title "Profile Settings"
- header_title "Profile Settings", profile_path unless header_title
-- sidebar "profile"
+- sidebar "dashboard"
+- nav "profile"
= render template: "layouts/application"
diff --git a/app/views/notify/closed_merge_request_email.html.haml b/app/views/notify/closed_merge_request_email.html.haml
index 574e8bfef24..81c7c88fc96 100644
--- a/app/views/notify/closed_merge_request_email.html.haml
+++ b/app/views/notify/closed_merge_request_email.html.haml
@@ -1,2 +1,2 @@
%p
- = "Merge Request ##{@merge_request.iid} was closed by #{@updated_by.name}"
+ = "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}"
diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml
index 59db86b08bc..b435067d5a6 100644
--- a/app/views/notify/closed_merge_request_email.text.haml
+++ b/app/views/notify/closed_merge_request_email.text.haml
@@ -1,4 +1,4 @@
-= "Merge Request ##{@merge_request.iid} was closed by #{@updated_by.name}"
+= "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}"
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
diff --git a/app/views/notify/merge_request_status_email.html.haml b/app/views/notify/merge_request_status_email.html.haml
index c9bf04f514e..41a320d6bd8 100644
--- a/app/views/notify/merge_request_status_email.html.haml
+++ b/app/views/notify/merge_request_status_email.html.haml
@@ -1,2 +1,2 @@
%p
- = "Merge Request ##{@merge_request.iid} was #{@mr_status} by #{@updated_by.name}"
+ = "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}"
diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml
index b96dd0fd8ab..7a5074a1dc3 100644
--- a/app/views/notify/merge_request_status_email.text.haml
+++ b/app/views/notify/merge_request_status_email.text.haml
@@ -1,4 +1,4 @@
-= "Merge Request ##{@merge_request.iid} was #{@mr_status} by #{@updated_by.name}"
+= "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}"
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
diff --git a/app/views/notify/merged_merge_request_email.html.haml b/app/views/notify/merged_merge_request_email.html.haml
index 6762fae7f64..fbe506d4f4d 100644
--- a/app/views/notify/merged_merge_request_email.html.haml
+++ b/app/views/notify/merged_merge_request_email.html.haml
@@ -1,2 +1,2 @@
%p
- = "Merge Request ##{@merge_request.iid} was merged"
+ = "Merge Request #{@merge_request.to_reference} was merged"
diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml
index 34dbc60e19b..bfbae01094f 100644
--- a/app/views/notify/merged_merge_request_email.text.haml
+++ b/app/views/notify/merged_merge_request_email.text.haml
@@ -1,4 +1,4 @@
-= "Merge Request ##{@merge_request.iid} was merged"
+= "Merge Request #{@merge_request.to_reference} was merged"
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb
index bdcca6e4ab7..d4aad8d1862 100644
--- a/app/views/notify/new_merge_request_email.text.erb
+++ b/app/views/notify/new_merge_request_email.text.erb
@@ -1,4 +1,4 @@
-New Merge Request #<%= @merge_request.iid %>
+New Merge Request <%= @merge_request.to_reference %>
<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)) %>
diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb
index 1d1411992a6..8cdab63829e 100644
--- a/app/views/notify/note_merge_request_email.text.erb
+++ b/app/views/notify/note_merge_request_email.text.erb
@@ -1,4 +1,4 @@
-New comment for Merge Request <%= @merge_request.iid %>
+New comment for Merge Request <%= @merge_request.to_reference %>
<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %>
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 6efd119f260..afd3d79321f 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -1,5 +1,4 @@
- page_title "Account"
-- header_title page_title, profile_account_path
- if current_user.ldap_user?
.alert.alert-info
diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml
index f630c03e5f6..9c404b6935f 100644
--- a/app/views/profiles/audit_log.html.haml
+++ b/app/views/profiles/audit_log.html.haml
@@ -1,5 +1,4 @@
- page_title "Audit Log"
-- header_title page_title, audit_log_profile_path
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 3f328f96cea..57527361eb6 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -1,5 +1,4 @@
- page_title "Emails"
-- header_title page_title, profile_emails_path
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index e0f8c9a5733..6a067a03535 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -1,5 +1,4 @@
- page_title "SSH Keys"
-- header_title page_title, profile_keys_path
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index a2a505c082b..7696f112bb3 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -1,5 +1,4 @@
- page_title "Notifications"
-- header_title page_title, profile_notifications_path
%div
- if @user.errors.any?
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index 5ac8a8b9d09..243428b690e 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -1,5 +1,4 @@
- page_title "Password"
-- header_title page_title, edit_profile_password_path
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index f80211669fb..bfe53be6854 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -1,5 +1,4 @@
- page_title 'Preferences'
-- header_title page_title, profile_preferences_path
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'row prepend-top-default js-preferences-form'} do |f|
.col-lg-3.profile-settings-sidebar
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 961b61d2e76..48b0dd6b121 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -9,4 +9,7 @@
= spinner
:javascript
- new Activities();
+ var activity = new Activities();
+ $(document).on('page:restore', function (event) {
+ activity.reloadActivities()
+ })
diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml
index b6074373e2b..0de019983ca 100644
--- a/app/views/projects/_builds_settings.html.haml
+++ b/app/views/projects/_builds_settings.html.haml
@@ -55,6 +55,9 @@
%li
gcovr (C/C++) -
%code ^TOTAL.*\s+(\d+\%)$
+ %li
+ tap --coverage-report=text-summary (Node.js) -
+ %code ^Statements\s*:\s*([^%]+)
.form-group
.col-sm-offset-2.col-sm-10
diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml
index 386d72e7787..66c30283c7a 100644
--- a/app/views/projects/_last_commit.html.haml
+++ b/app/views/projects/_last_commit.html.haml
@@ -1,9 +1,8 @@
.project-last-commit
- - ci_commit = project.ci_commit(commit.sha)
- - if ci_commit
- = link_to ci_status_path(ci_commit), class: "ci-status ci-#{ci_commit.status}" do
- = ci_status_icon(ci_commit)
- = ci_status_label(ci_commit)
+ - if commit.status
+ = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{commit.status}" do
+ = ci_icon_for_status(commit.status)
+ = ci_label_for_status(commit.status)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
index d1191928d4f..a9908eaecca 100644
--- a/app/views/projects/_readme.html.haml
+++ b/app/views/projects/_readme.html.haml
@@ -9,7 +9,7 @@
- else
.gray-content-block.second-block.center
%h3.page-title
- This project does not have README yet
+ This project does not have a README yet
- if can?(current_user, :push_code, @project)
%p
A
@@ -18,5 +18,5 @@
distributed with computer software, forming part of its documentation.
%p
We recommend you to
- = link_to "add README", new_readme_path, class: 'underlined-link'
+ = link_to "add a README", new_readme_path, class: 'underlined-link'
file to the repository and GitLab will render it here instead of this message.
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index f8b6fa253c4..fefa652a3da 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -13,7 +13,11 @@
required: true, class: 'form-control new-file-name'
.pull-right
- = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
+ .license-selector.js-license-selector.hide
+ = select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name}
+
+ .encoding-selector
+ = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
.file-content.code
%pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]}
diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml
index 38e62c81fed..5926d181ba3 100644
--- a/app/views/projects/blob/diff.html.haml
+++ b/app/views/projects/blob/diff.html.haml
@@ -1,17 +1,17 @@
- if @lines.present?
- if @form.unfold? && @form.since != 1 && !@form.bottom?
- %tr.line_holder{ id: @form.since }
+ %tr.line_holder
= render "projects/diffs/match_line", { line: @match_line,
line_old: @form.since, line_new: @form.since, bottom: false, new_file: false }
- @lines.each_with_index do |line, index|
- line_new = index + @form.since
- line_old = line_new - @form.offset
- %tr.line_holder
+ %tr.line_holder{ id: line_old }
%td.old_line.diff-line-num{ data: { linenumber: line_old } }
- = link_to raw(line_old), "#"
+ = link_to raw(line_old), "##{line_old}"
%td.new_line.diff-line-num{ data: { linenumber: line_old } }
- = link_to raw(line_new) , "#"
+ = link_to raw(line_new) , "##{line_old}"
%td.line_content.noteable_line==#{' ' * @form.indent}#{line}
- if @form.unfold? && @form.bottom? && @form.to < @blob.loc
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 1dd2b5c0af7..0459699432e 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -14,5 +14,5 @@
cancel_path: namespace_project_tree_path(@project.namespace, @project, @id)
:javascript
- blob = new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null)
+ blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}")
new NewCommitForm($('.js-new-blob-form'))
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index aa85f495e39..0406fc21d77 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -58,6 +58,6 @@
%th Coverage
%th
- = render @builds, commit_sha: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled?
+ = render @builds, commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled?
= paginate @builds, theme: 'gitlab'
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index b02aee3db21..99d72aa7935 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -10,10 +10,10 @@
- merge_request = @build.merge_request
- if merge_request
via
- = link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request)
+ = link_to "merge request #{merge_request.to_reference}", merge_request_path(merge_request)
#up-build-trace
- - builds = @build.commit.matrix_builds(@build)
+ - builds = @build.commit.builds.latest.to_a
- if builds.size > 1
%ul.nav-links.no-top.no-bottom
- builds.each do |build|
@@ -110,7 +110,7 @@
= icon('folder-open')
Browse
- .build-widget
+ .build-widget.build-controls
%h4.title
Build ##{@build.id}
- if can?(current_user, :update_build, @project)
@@ -127,6 +127,9 @@
data: { confirm: 'Are you sure you want to erase this build?' } do
= icon('eraser')
Erase
+ - if @build.has_trace?
+ = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build),
+ class: 'btn btn-sm btn-success', target: '_blank'
.clearfix
- if @build.duration
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 2cf9115e4dd..e123eb1cc7a 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -19,11 +19,12 @@
%td
= link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
- %td
- - if build.ref
- = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
- - else
- .light none
+ - if defined?(ref) && ref
+ %td
+ - if build.ref
+ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
+ - else
+ .light none
- if defined?(runner) && runner
%td
@@ -48,6 +49,8 @@
%span.label.label-info triggered
- if build.try(:allow_failure)
%span.label.label-danger allowed to fail
+ - if defined?(retried) && retried
+ %span.label.label-warning retried
%td.duration
- if build.duration
diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml
index 003b7c18d0e..5c9a319edeb 100644
--- a/app/views/projects/commit/_builds.html.haml
+++ b/app/views/projects/commit/_builds.html.haml
@@ -1,67 +1,2 @@
-.gray-content-block.middle-block
- .pull-right
- - if can?(current_user, :update_build, @ci_commit.project)
- - if @ci_commit.builds.latest.failed.any?(&:retryable?)
- = link_to "Retry failed", retry_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post
-
- - if @ci_commit.builds.running_or_pending.any?
- = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
-
- .oneline
- = pluralize @statuses.count(:id), "build"
- - if defined?(link_to_commit) && link_to_commit
- for commit
- = link_to @ci_commit.short_sha, namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: "monospace"
- - if @ci_commit.duration > 0
- in
- = time_interval_in_words @ci_commit.duration
-
-- if @ci_commit.yaml_errors.present?
- .bs-callout.bs-callout-danger
- %h4 Found errors in your .gitlab-ci.yml:
- %ul
- - @ci_commit.yaml_errors.split(",").each do |error|
- %li= error
- You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
-
-- if @ci_commit.project.builds_enabled? && !@ci_commit.ci_yaml_file
- .bs-callout.bs-callout-warning
- \.gitlab-ci.yml not found in this commit
-
-.table-holder
- %table.table.builds
- %thead
- %tr
- %th Status
- %th Build ID
- %th Ref
- %th Stage
- %th Name
- %th Duration
- %th Finished at
- - if @ci_commit.project.build_coverage_enabled?
- %th Coverage
- %th
- - @ci_commit.refs.each do |ref|
- - builds = @ci_commit.statuses.for_ref(ref).latest.ordered
- = render builds, coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true
-
-- if @ci_commit.retried.any?
- .gray-content-block.second-block
- Retried builds
-
- .table-holder
- %table.table.builds
- %thead
- %tr
- %th Status
- %th Build ID
- %th Ref
- %th Stage
- %th Name
- %th Duration
- %th Finished at
- - if @ci_commit.project.build_coverage_enabled?
- %th Coverage
- %th
- = render @ci_commit.retried, coverage: @ci_commit.project.build_coverage_enabled?, stage: true
+- @ci_commits.each do |ci_commit|
+ = render "ci_commit", ci_commit: ci_commit
diff --git a/app/views/projects/commit/_revert.html.haml b/app/views/projects/commit/_change.html.haml
index 52ca3ed5b14..44ef1fdbbe3 100644
--- a/app/views/projects/commit/_revert.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -1,13 +1,21 @@
-#modal-revert-commit.modal
+- case type.to_s
+- when 'revert'
+ - label = 'Revert'
+ - target_label = 'Revert in branch'
+- when 'cherry-pick'
+ - label = 'Cherry-pick'
+ - target_label = 'Pick into branch'
+
+.modal{id: "modal-#{type}-commit"}
.modal-dialog
.modal-content
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3.page-title== Revert this #{revert_commit_type(commit)}
+ %h3.page-title== #{label} this #{commit.change_type_title}
.modal-body
- = form_tag revert_namespace_project_commit_path(@project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-requires-input' do
+ = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do
.form-group.branch
- = label_tag 'target_branch', 'Revert in branch', class: 'control-label'
+ = label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10
= select_tag "target_branch", grouped_options_refs, class: "select2 select2-sm js-target-branch"
- if can?(current_user, :push_code, @project)
@@ -20,7 +28,7 @@
- else
= hidden_field_tag 'create_merge_request', 1
.form-actions
- = submit_tag "Revert", class: 'btn btn-create'
+ = submit_tag label, class: 'btn btn-create'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- unless can?(current_user, :push_code, @project)
@@ -28,4 +36,4 @@
= commit_in_fork_help
:javascript
- new NewCommitForm($('.js-create-dir-form'))
+ new NewCommitForm($('.js-#{type}-form'))
diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml
new file mode 100644
index 00000000000..25714e6cb47
--- /dev/null
+++ b/app/views/projects/commit/_ci_commit.html.haml
@@ -0,0 +1,69 @@
+.gray-content-block.middle-block
+ .pull-right
+ - if can?(current_user, :update_build, @project)
+ - if ci_commit.builds.latest.failed.any?(&:retryable?)
+ = link_to "Retry failed", retry_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post
+
+ - if ci_commit.builds.running_or_pending.any?
+ = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
+
+ .oneline
+ = pluralize ci_commit.statuses.count(:id), "build"
+ - if ci_commit.ref
+ for
+ %span.label.label-info
+ = ci_commit.ref
+ - if defined?(link_to_commit) && link_to_commit
+ for commit
+ = link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace"
+ - if ci_commit.duration > 0
+ in
+ = time_interval_in_words ci_commit.duration
+
+- if ci_commit.yaml_errors.present?
+ .bs-callout.bs-callout-danger
+ %h4 Found errors in your .gitlab-ci.yml:
+ %ul
+ - ci_commit.yaml_errors.split(",").each do |error|
+ %li= error
+ You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
+
+- if @project.builds_enabled? && !ci_commit.ci_yaml_file
+ .bs-callout.bs-callout-warning
+ \.gitlab-ci.yml not found in this commit
+
+.table-holder
+ %table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Stage
+ %th Name
+ %th Duration
+ %th Finished at
+ - if @project.build_coverage_enabled?
+ %th Coverage
+ %th
+ - builds = ci_commit.statuses.latest.ordered
+ = render builds, coverage: @project.build_coverage_enabled?, stage: true, ref: false, allow_retry: true
+
+- if ci_commit.retried.any?
+ .gray-content-block.second-block
+ Retried builds
+
+ .table-holder
+ %table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Ref
+ %th Stage
+ %th Name
+ %th Duration
+ %th Finished at
+ - if @project.build_coverage_enabled?
+ %th Coverage
+ %th
+ = render ci_commit.retried, coverage: @project.build_coverage_enabled?, stage: true, ref: false
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 71995fcc487..3d7c18a5f58 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -18,6 +18,7 @@
Browse Files
- unless @commit.has_been_reverted?(current_user)
= revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id))
+ = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id))
%div
%p
@@ -42,12 +43,12 @@
- @commit.parents.each do |parent|
= link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace"
-- if @ci_commit
+- if @commit.status
.pull-right
- = link_to ci_status_path(@ci_commit), class: "ci-status ci-#{@ci_commit.status}" do
- = ci_status_icon(@ci_commit)
+ = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status ci-#{@commit.status}" do
+ = ci_icon_for_status(@commit.status)
build:
- = ci_status_label(@ci_commit)
+ = ci_label_for_status(@commit.status)
.commit-info-row.branches
%i.fa.fa-spinner.fa-spin
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 21e186120c3..e5e3d696035 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -5,7 +5,7 @@
.prepend-top-default
= render "commit_box"
-- if @ci_commit
+- if @commit.status
= render "ci_menu"
- else
%div.block-connector
@@ -13,4 +13,5 @@
diff_refs: @diff_refs
= render "projects/notes/notes_with_form"
- if can_collaborate_with_project?
- = render "projects/commit/revert", commit: @commit, title: @commit.title
+ - %w(revert cherry-pick).each do |type|
+ = render "projects/commit/change", type: type, commit: @commit, title: @commit.title
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 34f27f1e793..c7d8c9a0d15 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -4,9 +4,8 @@
- notes = commit.notes
- note_count = notes.user.count
-- ci_commit = project.ci_commit(commit.sha)
- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count]
-- cache_key.push(ci_commit.status) if ci_commit
+- cache_key.push(commit.status) if commit.status
= cache(cache_key) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
@@ -17,8 +16,8 @@
%a.text-expander.js-toggle-button ...
.pull-right
- - if ci_commit
- = render_ci_status(ci_commit)
+ - if commit.status
+ = render_ci_status(commit)
= clipboard_button(clipboard_text: commit.id)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
@@ -31,5 +30,5 @@
by
= commit_author_link(commit, avatar: true, size: 24)
.committed_ago
- #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} &nbsp;
+ #{time_ago_with_tooltip(commit.committed_date)} &nbsp;
= link_to_browse_code(project, commit)
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index c52cf25d40a..bcdb09208aa 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -39,4 +39,4 @@
= spinner
:javascript
- CommitsList.init("#{@ref}", #{@limit});
+ CommitsList.init(#{@limit});
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 6ad7b05155a..52d093871b4 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -14,8 +14,10 @@
%p
If you already have files you can push them using command line instructions below.
%p
- Otherwise you can start with
- = link_to "adding README", new_readme_path, class: 'underlined-link'
+ Otherwise you can start with adding a
+ = link_to "README", new_readme_path, class: 'underlined-link'
+ or a
+ = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link'
file to this project.
- if can?(current_user, :push_code, @project)
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index c15386b4883..f21c864e35c 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -15,12 +15,13 @@
- if defined?(commit_sha) && commit_sha
%td
= link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace"
-
- %td
- - if generic_commit_status.ref
- = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref)
- - else
- .light none
+
+ - if defined?(ref) && ref
+ %td
+ - if generic_commit_status.ref
+ = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref)
+ - else
+ .light none
- if defined?(runner) && runner
%td
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml
index e39224d86c6..aae3abcad4b 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/index.html.haml
@@ -74,16 +74,15 @@
.panel.panel-default
.panel-heading
Webhooks (#{@hooks.count})
- %ul.well-list
+ %ul.content-list
- @hooks.each do |hook|
%li
- .pull-right
+ .controls
= link_to 'Test Hook', test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped"
= link_to 'Remove', namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
- .clearfix
- %span.monospace= hook.url
- %p
+ .monospace= hook.url
+ %div
- %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray= trigger.titleize
- SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
+ %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 7a8009f6da4..9ad86ed71c9 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -48,6 +48,11 @@
= link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
= icon('clock-o')
= issue.milestone.title
+ - if issue.due_date
+ %span{class: "#{'cred' if issue.overdue?}"}
+ &nbsp;
+ = icon('calendar')
+ = issue.due_date.to_s(:medium)
- if issue.labels.any?
&nbsp;
- issue.labels.each do |label|
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index b10cd03515f..bdfa0c7009e 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -5,7 +5,7 @@
- @related_branches.each do |branch|
%li
- sha = @project.repository.find_branch(branch).target
- - ci_commit = @project.ci_commit(sha) if sha
+ - ci_commit = @project.ci_commit(sha, branch) if sha
- if ci_commit
%span.related-branch-ci-status
= render_ci_status(ci_commit)
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 7125d7d9d1c..8d05060f563 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -86,8 +86,10 @@
= spinner
= render 'shared/issuable/sidebar', issuable: @merge_request
-- if @merge_request.can_be_reverted?
- = render "projects/commit/revert", commit: @merge_request.merge_commit, title: @merge_request.title
+- if @merge_request.can_be_reverted?(current_user)
+ = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title
+- if @merge_request.can_be_cherry_picked?
+ = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title
:javascript
var merge_request;
diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml
index fc62bb5bce9..b31ea5e5321 100644
--- a/app/views/projects/merge_requests/edit.html.haml
+++ b/app/views/projects/merge_requests/edit.html.haml
@@ -1,7 +1,7 @@
-- page_title "Edit", "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+- page_title "Edit", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
= render "header_title"
%h3.page-title
- Edit Merge Request ##{@merge_request.iid}
+ Edit Merge Request #{@merge_request.to_reference}
%hr
= render 'form'
diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml
index fc03ee73a3d..f5bf16ef3ad 100644
--- a/app/views/projects/merge_requests/invalid.html.haml
+++ b/app/views/projects/merge_requests/invalid.html.haml
@@ -1,4 +1,4 @@
-- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+- page_title "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
= render "header_title"
.merge-request
diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml
index 307a75d02ca..a116ffe2e15 100644
--- a/app/views/projects/merge_requests/show/_builds.html.haml
+++ b/app/views/projects/merge_requests/show/_builds.html.haml
@@ -1 +1,2 @@
-= render "projects/commit/builds", link_to_commit: true
+= render "projects/commit/ci_commit", ci_commit: @ci_commit, link_to_commit: true
+
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index 0a99e8c9591..36c275e8be1 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -24,9 +24,9 @@
%li{ class: issue_button_visibility(@merge_request, false) }
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
%li
- = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit', id: 'edit_merge_request'
+ = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit'
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request'
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
- = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit", id: 'edit_merge_request' do
+ = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit" do
= icon('pencil-square-o')
Edit
diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml
index 3abae9f0bf6..ec4beae9727 100644
--- a/app/views/projects/merge_requests/widget/_merged.html.haml
+++ b/app/views/projects/merge_requests/widget/_merged.html.haml
@@ -44,3 +44,8 @@
$('.remove_source_branch_in_progress').hide();
$('.remove_source_branch_widget.failed').show();
});
+ - else
+ %p
+ The changes were merged into
+ #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
+ = render 'projects/merge_requests/widget/merged_buttons'
diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml
index 85a3a6ba9e2..56167509af9 100644
--- a/app/views/projects/merge_requests/widget/_merged_buttons.haml
+++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml
@@ -1,11 +1,14 @@
-- source_branch_exists = local_assigns.fetch(:source_branch_exists, false)
-- mr_can_be_reverted = @merge_request.can_be_reverted?
+- can_remove_source_branch = local_assigns.fetch(:source_branch_exists, false) && @merge_request.can_remove_source_branch?(current_user)
+- mr_can_be_reverted = @merge_request.can_be_reverted?(current_user)
+- mr_can_be_cherry_picked = @merge_request.can_be_cherry_picked?
-- if source_branch_exists || mr_can_be_reverted
+- if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked
.btn-group
- - if source_branch_exists
+ - if can_remove_source_branch
= link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-grouped btn-sm remove_source_branch" do
= icon('trash-o')
Remove Source Branch
- if mr_can_be_reverted
= revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm')
+ - if mr_can_be_cherry_picked
+ = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm')
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 6e9ecdf7649..aeb7c1d5ee4 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -8,7 +8,9 @@
.note-header
= link_to_member(note.project, note.author, avatar: false)
.inline.note-headline-light
- = "#{note.author.to_reference} commented"
+ = "#{note.author.to_reference}"
+ - if !note.system
+ commented
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
.note-actions
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 6a37f444bb7..9fa4127c948 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -1,7 +1,10 @@
%h3 Shared runners
-.bs-callout.bs-callout-warning
- GitLab Runners do not offer secure isolation between projects that they do builds for. You are TRUSTING all GitLab users who can push code to project A, B or C to run shell scripts on the machine hosting runner X.
+.bs-callout.bs-callout-warning.shared-runners-description
+ - if shared_runners_text.present?
+ = markdown(shared_runners_text, pipeline: 'plain_markdown')
+ - else
+ Shared runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is on GitLab.com).
%hr
- if @project.shared_runners_enabled?
= link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-warning', method: :post do
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 4310f038fc9..d854ac21725 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -36,9 +36,9 @@
%li
= link_to 'Changelog', changelog_path(@project)
- - if @repository.license
+ - if @repository.license_blob
%li
- = link_to 'License', license_path(@project)
+ = link_to license_short_name(@project), license_path(@project)
- if @repository.contribution_guide
%li
@@ -47,15 +47,15 @@
- if current_user && can_push_branch?(@project, @project.default_branch)
- unless @repository.changelog
%li.missing
- = link_to add_changelog_path(@project) do
+ = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do
Add Changelog
- - unless @repository.license
+ - unless @repository.license_blob
%li.missing
- = link_to add_license_path(@project) do
+ = link_to add_special_file_path(@project, file_name: 'LICENSE') do
Add License
- unless @repository.contribution_guide
%li.missing
- = link_to add_contribution_guide_path(@project) do
+ = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
Add Contribution guide
- if @repository.commit
diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml
index df16f503570..a585147ddd1 100644
--- a/app/views/repository_check_mailer/notify.html.haml
+++ b/app/views/repository_check_mailer/notify.html.haml
@@ -3,3 +3,6 @@
%p
= link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1)
+
+%p
+ You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}.
diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml
index 02f3f80288a..93db151329e 100644
--- a/app/views/repository_check_mailer/notify.text.haml
+++ b/app/views/repository_check_mailer/notify.text.haml
@@ -1,3 +1,6 @@
#{@message}.
\
View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)}
+
+You are receiving this message because you are a GitLab administrator
+for #{Gitlab.config.gitlab.url}.
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index faeb2b55c6f..333f6533213 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -2,7 +2,7 @@
%h4
= link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do
%span.term.str-truncated= merge_request.title
- .pull-right ##{merge_request.iid}
+ .pull-right #{merge_request.to_reference}
- if merge_request.description.present?
.description.term
= preserve do
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index b38c5e18efb..9ce5562e667 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -2,4 +2,4 @@
%span.label-name
= link_to_label(label, tooltip: false)
%span.prepend-left-10
- = markdown(label.description, pipeline: :single_line)
+ = markdown(label.description, pipeline: :single_line) \ No newline at end of file
diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml
new file mode 100644
index 00000000000..dc89e36419c
--- /dev/null
+++ b/app/views/shared/_labels_row.html.haml
@@ -0,0 +1,3 @@
+- labels.each do |label|
+ %span.label-row
+ = link_to_label(label, tooltip: false)
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index fc935166bf6..4eaf7c2a025 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -62,6 +62,14 @@
%strong Build events
%p.light
This url will be triggered when a build status changes
+ - if @service.supported_events.include?("wiki_page")
+ %div
+ = form.check_box :wiki_page_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :wiki_page_events, class: 'list-label' do
+ %strong Wiki Page events
+ %p.light
+ This url will be triggered when a wiki page is created/updated
- @service.fields.each do |field|
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index e3a6a5a68b6..d327bd0a96f 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -20,6 +20,11 @@
= sort_title_milestone_soon
= link_to page_filter_path(sort: sort_value_milestone_later) do
= sort_title_milestone_later
+ - if controller.controller_name == 'issues' || controller.action_name == 'issues'
+ = link_to page_filter_path(sort: sort_value_due_date_soon) do
+ = sort_title_due_date_soon
+ = link_to page_filter_path(sort: sort_value_due_date_later) do
+ = sort_title_due_date_later
= link_to page_filter_path(sort: sort_value_upvotes) do
= sort_title_upvotes
= link_to page_filter_path(sort: sort_value_downvotes) do
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 921eaefd79a..fc1101646fb 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -23,6 +23,7 @@
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown"
+
.pull-right
= render 'shared/sort_dropdown'
@@ -46,9 +47,10 @@
.filter-item.inline
= button_tag "Update issues", class: "btn update_selected_issues btn-save"
-- if @label
- .gray-content-block.second-block
- = render "shared/label_row", label: @label
+ - if !@labels.nil?
+ .gray-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) }
+ - if @labels.any?
+ = render "shared/labels_row", labels: @labels
:javascript
new UsersSelect();
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index bae15b7f844..18b091df39b 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -75,8 +75,9 @@
= f.label :label_ids, "Labels", class: 'control-label'
.col-sm-10{ class: ('issuable-form-padding-top' if !has_labels) }
- if has_labels
- = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
- { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
+ .issuable-form-select-holder
+ = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
+ { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
- else
%span.light No labels yet.
&nbsp;
@@ -88,9 +89,10 @@
.form-group
= label_tag :move_to_project_id, 'Move', class: 'control-label'
.col-sm-10
- - projects = project_options(issuable, current_user, ability: :admin_issue)
- = select_tag(:move_to_project_id, projects, include_blank: true,
- class: 'select2', data: { placeholder: 'Select project' })
+ .issuable-form-select-holder
+ - projects = project_options(issuable, current_user, ability: :admin_issue)
+ = select_tag(:move_to_project_id, projects, include_blank: true,
+ class: 'select2', data: { placeholder: 'Select project' })
&nbsp;
%span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default',
title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
@@ -102,13 +104,15 @@
.form-group
= f.label :source_branch, class: 'control-label'
.col-sm-10
- = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
+ .issuable-form-select-holder
+ = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
.form-group
= f.label :target_branch, class: 'control-label'
.col-sm-10
- = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} })
+ .issuable-form-select-holder
+ = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} })
- if @merge_request.new_record?
- %p.help-block
+ &nbsp;
= link_to 'Change branches', mr_change_branches_path(@merge_request)
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index f722e61eeac..61fd1e9c335 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -1,44 +1,14 @@
- if params[:label_name].present?
- = hidden_field_tag(:label_name, params[:label_name])
+ - if params[:label_name].respond_to?('any?')
+ - params[:label_name].each do |label|
+ = hidden_field_tag "label_name[]", label, id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}}
+ %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-multiselect.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name[]", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}}
%span.dropdown-toggle-text
- = h(params[:label_name].presence || "Label")
+ = h(multi_label_name(params[:label_name], "Label"))
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
- .dropdown-page-one
- = dropdown_title("Filter by label")
- = dropdown_filter("Search labels")
- = dropdown_content
- - if @project
- = dropdown_footer do
- %ul.dropdown-footer-list
- - if can? current_user, :admin_label, @project
- %li
- %a.dropdown-toggle-page{href: "#"}
- Create new
- %li
- = link_to namespace_project_labels_path(@project.namespace, @project) do
- - if can? current_user, :admin_label, @project
- Manage labels
- - else
- View labels
+ = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label" }
- if can? current_user, :admin_label, @project and @project
- .dropdown-page-two.dropdown-new-label
- = dropdown_title("Create new label", back: true)
- = dropdown_content do
- .dropdown-labels-error.js-label-error
- %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"}
- .suggest-colors.suggest-colors-dropdown
- - suggested_colors.each do |color|
- = link_to '#', style: "background-color: #{color}", data: { color: color } do
- &nbsp
- .dropdown-label-color-input
- .dropdown-label-color-preview.js-dropdown-label-color-preview
- %input#new_label_color.dropdown-input-field{ type: "text" }
- .clearfix
- %button.btn.btn-primary.pull-left.js-new-label-btn{type: "button"}
- Create
- %button.btn.btn-default.pull-right.js-cancel-label-btn{type: "button"}
- Cancel
+ = render partial: "shared/issuable/label_page_create"
= dropdown_loading
diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml
new file mode 100644
index 00000000000..3bc57d3d2ac
--- /dev/null
+++ b/app/views/shared/issuable/_label_page_create.html.haml
@@ -0,0 +1,17 @@
+.dropdown-page-two.dropdown-new-label
+ = dropdown_title("Create new label", back: true)
+ = dropdown_content do
+ .dropdown-labels-error.js-label-error
+ %input#new_label_name.default-dropdown-input{ type: "text", placeholder: "Name new label" }
+ .suggest-colors.suggest-colors-dropdown
+ - suggested_colors.each do |color|
+ = link_to '#', style: "background-color: #{color}", data: { color: color } do
+ &nbsp
+ .dropdown-label-color-input
+ .dropdown-label-color-preview.js-dropdown-label-color-preview
+ %input#new_label_color.default-dropdown-input{ type: "text" }
+ .clearfix
+ %button.btn.btn-primary.pull-left.js-new-label-btn{ type: "button" }
+ Create
+ %button.btn.btn-default.pull-right.js-cancel-label-btn{ type: "button" }
+ Cancel
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
new file mode 100644
index 00000000000..7f4867417f7
--- /dev/null
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -0,0 +1,20 @@
+- title = local_assigns.fetch(:title, 'Assign labels')
+- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels')
+.dropdown-page-one
+ = dropdown_title(title)
+ = dropdown_filter(filter_placeholder)
+ = dropdown_content
+ - if @project
+ = dropdown_footer do
+ %ul.dropdown-footer-list
+ - if can? current_user, :admin_label, @project
+ %li
+ %a.dropdown-toggle-page{href: "#"}
+ Create new
+ %li
+ = link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do
+ - if can? current_user, :admin_label, @project
+ Manage labels
+ - else
+ View labels
+ = dropdown_loading \ No newline at end of file
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index a6970b7eebb..1d9b09a5ef1 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -4,22 +4,22 @@
- else
- page_context_word = 'issues'
%li{class: ("active" if params[:state] == 'opened')}
- = link_to page_filter_path(state: 'opened'), title: "Filter by #{page_context_word} that are currently opened." do
+ = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do
#{state_filters_text_for(:opened, @project)}
- if defined?(type) && type == :merge_requests
%li{class: ("active" if params[:state] == 'merged')}
- = link_to page_filter_path(state: 'merged'), title: 'Filter by merge requests that are currently merged.' do
+ = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do
#{state_filters_text_for(:merged, @project)}
%li{class: ("active" if params[:state] == 'closed')}
- = link_to page_filter_path(state: 'closed'), title: 'Filter by merge requests that are currently closed and unmerged.' do
+ = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do
#{state_filters_text_for(:closed, @project)}
- else
%li{class: ("active" if params[:state] == 'closed')}
- = link_to page_filter_path(state: 'closed'), title: 'Filter by issues that are currently closed.' do
+ = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do
#{state_filters_text_for(:closed, @project)}
%li{class: ("active" if params[:state] == 'all')}
- = link_to page_filter_path(state: 'all'), title: "Show all #{page_context_word}." do
+ = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do
#{state_filters_text_for(:all, @project)}
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 08bfd93f4e6..991d4d675dd 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -10,14 +10,14 @@
= sidebar_gutter_toggle_icon
.issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'}
- if prev_issuable = prev_issuable_for(issuable)
- = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn'
+ = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn issuable-pager'
- else
- %a.btn.btn-default.disabled{href: '#'}
+ %a.btn.btn-default.issuable-pager.disabled{href: '#'}
Prev
- if next_issuable = next_issuable_for(issuable)
- = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn'
+ = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn issuable-pager'
- else
- %a.btn.btn-default.disabled{href: '#'}
+ %a.btn.btn-default.issuable-pager.disabled{href: '#'}
Next
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
@@ -58,7 +58,7 @@
- if issuable.milestone
= issuable.milestone.title
- else
- No
+ None
.title.hide-collapsed
Milestone
= icon('spinner spin', class: 'block-loading')
@@ -75,6 +75,34 @@
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
= dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
+ - if issuable.has_attribute?(:due_date)
+ .block.due_date
+ .sidebar-collapsed-icon
+ = icon('calendar')
+ %span.js-due-date-sidebar-value
+ = issuable.due_date.try(:to_s, :medium) || 'None'
+ .title.hide-collapsed
+ Due date
+ = icon('spinner spin', class: 'block-loading')
+ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ = link_to 'Edit', '#', class: 'edit-link pull-right'
+ .value.bold.hide-collapsed
+ - if issuable.due_date
+ = issuable.due_date.to_s(:medium)
+ - else
+ .light None
+ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ .selectbox.hide-collapsed
+ = f.hidden_field :due_date, value: issuable.due_date
+ .dropdown
+ %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } }
+ %span.dropdown-toggle-text Due date
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-menu-due-date
+ = dropdown_title('Due date')
+ = dropdown_content do
+ .js-due-date-calendar
+
- if issuable.project.labels.any?
.block.labels
.sidebar-collapsed-icon
@@ -101,23 +129,9 @@
Label
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
- .dropdown-page-one
- = dropdown_title("Assign labels")
- = dropdown_filter("Search labels")
- = dropdown_content
- - if @project
- = dropdown_footer do
- %ul.dropdown-footer-list
- - if can? current_user, :admin_label, @project
- %li
- %a.dropdown-toggle-page{href: "#"}
- Create new
- %li
- = link_to namespace_project_labels_path(@project.namespace, @project) do
- - if can? current_user, :admin_label, @project
- Manage labels
- - else
- View labels
+ = render partial: "shared/issuable/label_page_default"
+ - if can? current_user, :admin_label, @project and @project
+ = render partial: "shared/issuable/label_page_create"
= render "shared/issuable/participants", participants: issuable.participants(current_user)
- if current_user
@@ -150,6 +164,7 @@
:javascript
new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}');
new LabelsSelect();
- new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}');
+ new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
new Subscription('.subscription')
new Sidebar();
+ new DueDateSelect();
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index e1127b2311c..47b66d44e43 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -23,5 +23,5 @@
- if assignee
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }),
- class: 'has-tooltip', data: { 'original-title' => "Assigned to #{sanitize(assignee.name)}", container: 'body' } do
+ class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
- image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '')
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 53ff8959bc8..ab8b022411d 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -6,9 +6,8 @@
- css_class = '' unless local_assigns[:css_class]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
-- ci_commit = project.ci_commit(project.commit.sha) if ci && !project.empty_repo? && project.commit
- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.3']
-- cache_key.push(ci_commit.status) if ci_commit
+- cache_key.push(project.commit.status) if project.commit.try(:status)
%li.project-row{ class: css_class }
= cache(cache_key) do
@@ -16,9 +15,9 @@
- if project.main_language
%span
= project.main_language
- - if ci_commit
+ - if project.commit.try(:status)
%span
- = render_ci_status(ci_commit)
+ = render_ci_status(project.commit)
- if forks
%span
= icon('code-fork')
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 0c4b6a5618b..3028491e5b6 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -6,8 +6,6 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
-= render 'shared/show_aside'
-
.user-profile
.cover-block
.cover-controls
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 9e1215b21a6..f3327ca9e61 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -39,7 +39,7 @@ class PostReceive
end
if Gitlab::Git.tag_ref?(ref)
- GitTagPushService.new.execute(post_received.project, @user, oldrev, newrev, ref)
+ GitTagPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
elsif Gitlab::Git.branch_ref?(ref)
GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
end
@@ -47,7 +47,7 @@ class PostReceive
end
private
-
+
def log(message)
Gitlab::GitLogger.error("POST-RECEIVE: #{message}")
end
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index e54ae86d06c..a76729e3c74 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -15,10 +15,10 @@ module RepositoryCheck
private
def check(project)
+ repositories = [project.repository]
+ repositories << project.wiki.repository if project.wiki_enabled?
# Use 'map do', not 'all? do', to prevent short-circuiting
- [project.repository, project.wiki.repository].map do |repository|
- git_fsck(repository.path_to_repo)
- end.all?
+ repositories.map { |repository| git_fsck(repository.path_to_repo) }.all?
end
def git_fsck(path)
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index 22fe51a4534..283936d0efc 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -107,6 +107,10 @@ if Gitlab::Metrics.enabled?
config.instrument_methods(const)
config.instrument_instance_methods(const)
end
+
+ # Instrument the classes used for checking if somebody has push access.
+ config.instrument_instance_methods(Gitlab::GitAccess)
+ config.instrument_instance_methods(Gitlab::GitAccessWiki)
end
GC::Profiler.enable
diff --git a/config/routes.rb b/config/routes.rb
index 46a25262844..79b62a0b1bb 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -549,6 +549,7 @@ Rails.application.routes.draw do
post :cancel_builds
post :retry_builds
post :revert
+ post :cherry_pick
end
end
@@ -668,6 +669,7 @@ Rails.application.routes.draw do
post :cancel
post :retry
post :erase
+ get :raw
end
resource :artifacts, only: [] do
diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb
index e3ca2b4eea3..b99d24a03c9 100644
--- a/db/fixtures/development/14_builds.rb
+++ b/db/fixtures/development/14_builds.rb
@@ -19,7 +19,7 @@ class Gitlab::Seeder::Builds
commits = @project.repository.commits('master', nil, 5)
commits_sha = commits.map { |commit| commit.raw.id }
commits_sha.map do |sha|
- @project.ensure_ci_commit(sha)
+ @project.ensure_ci_commit(sha, 'master')
end
rescue
[]
diff --git a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb
index 003169c13c6..d7b00e3d6ed 100644
--- a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb
+++ b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb
@@ -4,6 +4,8 @@ class AddTrigramIndexesForSearching < ActiveRecord::Migration
def up
return unless Gitlab::Database.postgresql?
+ create_trigrams_extension
+
unless trigrams_enabled?
raise 'You must enable the pg_trgm extension. You can do so by running ' \
'"CREATE EXTENSION pg_trgm;" as a PostgreSQL super user, this must be ' \
@@ -37,6 +39,15 @@ class AddTrigramIndexesForSearching < ActiveRecord::Migration
row && row['enabled'] == 't' ? true : false
end
+ def create_trigrams_extension
+ # This may not work if the user doesn't have permission. We attempt in
+ # case we do have permission, particularly for test/dev environments.
+ begin
+ enable_extension 'pg_trgm'
+ rescue
+ end
+ end
+
def to_index
{
ci_runners: [:token, :description],
diff --git a/db/migrate/20160227120001_add_event_field_for_web_hook.rb b/db/migrate/20160227120001_add_event_field_for_web_hook.rb
new file mode 100644
index 00000000000..65f2a47bb3c
--- /dev/null
+++ b/db/migrate/20160227120001_add_event_field_for_web_hook.rb
@@ -0,0 +1,5 @@
+class AddEventFieldForWebHook < ActiveRecord::Migration
+ def change
+ add_column :web_hooks, :wiki_page_events, :boolean, default: false, null: false
+ end
+end
diff --git a/db/migrate/20160227120047_add_event_to_services.rb b/db/migrate/20160227120047_add_event_to_services.rb
new file mode 100644
index 00000000000..f5040d770de
--- /dev/null
+++ b/db/migrate/20160227120047_add_event_to_services.rb
@@ -0,0 +1,5 @@
+class AddEventToServices < ActiveRecord::Migration
+ def change
+ add_column :services, :wiki_page_events, :boolean, default: true
+ end
+end
diff --git a/db/migrate/20160310124959_add_due_date_to_issues.rb b/db/migrate/20160310124959_add_due_date_to_issues.rb
new file mode 100644
index 00000000000..ec08bd9fdfa
--- /dev/null
+++ b/db/migrate/20160310124959_add_due_date_to_issues.rb
@@ -0,0 +1,6 @@
+class AddDueDateToIssues < ActiveRecord::Migration
+ def change
+ add_column :issues, :due_date, :date
+ add_index :issues, :due_date
+ end
+end
diff --git a/db/migrate/20160412173416_add_fields_to_ci_commit.rb b/db/migrate/20160412173416_add_fields_to_ci_commit.rb
new file mode 100644
index 00000000000..125956a3ddd
--- /dev/null
+++ b/db/migrate/20160412173416_add_fields_to_ci_commit.rb
@@ -0,0 +1,8 @@
+class AddFieldsToCiCommit < ActiveRecord::Migration
+ def change
+ add_column :ci_commits, :status, :string
+ add_column :ci_commits, :started_at, :timestamp
+ add_column :ci_commits, :finished_at, :timestamp
+ add_column :ci_commits, :duration, :integer
+ end
+end
diff --git a/db/migrate/20160412173417_update_ci_commit.rb b/db/migrate/20160412173417_update_ci_commit.rb
new file mode 100644
index 00000000000..fd92444dbac
--- /dev/null
+++ b/db/migrate/20160412173417_update_ci_commit.rb
@@ -0,0 +1,35 @@
+class UpdateCiCommit < ActiveRecord::Migration
+ # This migration can be run online, but needs to be executed for the second time after restarting Unicorn workers
+ # Otherwise Offline migration should be used.
+ def change
+ execute("UPDATE ci_commits SET status=#{status}, ref=#{ref}, tag=#{tag} WHERE status IS NULL")
+ end
+
+ private
+
+ def status
+ builds = '(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id)'
+ success = "(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id AND status='success')"
+ ignored = "(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id AND (status='failed' OR status='canceled') AND allow_failure)"
+ pending = "(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id AND status='pending')"
+ running = "(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id AND status='running')"
+ canceled = "(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id AND status='canceled')"
+
+ "(CASE
+ WHEN #{builds}=0 THEN 'skipped'
+ WHEN #{builds}=#{success}+#{ignored} THEN 'success'
+ WHEN #{builds}=#{pending} THEN 'pending'
+ WHEN #{builds}=#{canceled} THEN 'canceled'
+ WHEN #{running}+#{pending}>0 THEN 'running'
+ ELSE 'failed'
+ END)"
+ end
+
+ def ref
+ '(SELECT ref FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id ORDER BY id DESC LIMIT 1)'
+ end
+
+ def tag
+ '(SELECT tag FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id ORDER BY id DESC LIMIT 1)'
+ end
+end
diff --git a/db/migrate/20160412173418_add_ci_commit_indexes.rb b/db/migrate/20160412173418_add_ci_commit_indexes.rb
new file mode 100644
index 00000000000..603d4a41610
--- /dev/null
+++ b/db/migrate/20160412173418_add_ci_commit_indexes.rb
@@ -0,0 +1,19 @@
+class AddCiCommitIndexes < ActiveRecord::Migration
+ disable_ddl_transaction!
+
+ def change
+ add_index :ci_commits, [:gl_project_id, :sha], index_options
+ add_index :ci_commits, [:gl_project_id, :status], index_options
+ add_index :ci_commits, [:status], index_options
+ end
+
+ private
+
+ def index_options
+ if Gitlab::Database.postgresql?
+ { algorithm: :concurrently }
+ else
+ { }
+ end
+ end
+end
diff --git a/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb b/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb
new file mode 100644
index 00000000000..d493044c67b
--- /dev/null
+++ b/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb
@@ -0,0 +1,5 @@
+class AddSharedRunnersTextToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :shared_runners_text, :text
+ end
+end
diff --git a/db/migrate/20160419120017_add_metrics_packet_size.rb b/db/migrate/20160419120017_add_metrics_packet_size.rb
new file mode 100644
index 00000000000..78c163d62ac
--- /dev/null
+++ b/db/migrate/20160419120017_add_metrics_packet_size.rb
@@ -0,0 +1,5 @@
+class AddMetricsPacketSize < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :metrics_packet_size, :integer, default: 1
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 42c261003bb..a743e682423 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160412140240) do
+ActiveRecord::Schema.define(version: 20160419120017) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -78,6 +78,8 @@ ActiveRecord::Schema.define(version: 20160412140240) do
t.boolean "email_author_in_body", default: false
t.integer "default_group_visibility"
t.boolean "repository_checks_enabled", default: true
+ t.integer "metrics_packet_size", default: 1
+ t.text "shared_runners_text"
end
create_table "audit_events", force: :cascade do |t|
@@ -169,14 +171,21 @@ ActiveRecord::Schema.define(version: 20160412140240) do
t.text "yaml_errors"
t.datetime "committed_at"
t.integer "gl_project_id"
+ t.string "status"
+ t.datetime "started_at"
+ t.datetime "finished_at"
+ t.integer "duration"
end
+ add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree
+ add_index "ci_commits", ["gl_project_id", "status"], name: "index_ci_commits_on_gl_project_id_and_status", using: :btree
add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree
add_index "ci_commits", ["project_id", "committed_at", "id"], name: "index_ci_commits_on_project_id_and_committed_at_and_id", using: :btree
add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree
add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree
add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree
add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree
+ add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree
create_table "ci_events", force: :cascade do |t|
t.integer "project_id"
@@ -420,6 +429,7 @@ ActiveRecord::Schema.define(version: 20160412140240) do
t.integer "moved_to_id"
t.boolean "confidential", default: false
t.datetime "deleted_at"
+ t.date "due_date"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -429,6 +439,7 @@ ActiveRecord::Schema.define(version: 20160412140240) do
add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree
add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
+ add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
@@ -716,37 +727,37 @@ ActiveRecord::Schema.define(version: 20160412140240) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "creator_id"
- t.boolean "issues_enabled", default: true, null: false
- t.boolean "wall_enabled", default: true, null: false
- t.boolean "merge_requests_enabled", default: true, null: false
- t.boolean "wiki_enabled", default: true, null: false
+ t.boolean "issues_enabled", default: true, null: false
+ t.boolean "wall_enabled", default: true, null: false
+ t.boolean "merge_requests_enabled", default: true, null: false
+ t.boolean "wiki_enabled", default: true, null: false
t.integer "namespace_id"
- t.string "issues_tracker", default: "gitlab", null: false
+ t.string "issues_tracker", default: "gitlab", null: false
t.string "issues_tracker_id"
- t.boolean "snippets_enabled", default: true, null: false
+ t.boolean "snippets_enabled", default: true, null: false
t.datetime "last_activity_at"
t.string "import_url"
- t.integer "visibility_level", default: 0, null: false
- t.boolean "archived", default: false, null: false
+ t.integer "visibility_level", default: 0, null: false
+ t.boolean "archived", default: false, null: false
t.string "avatar"
t.string "import_status"
- t.float "repository_size", default: 0.0
- t.integer "star_count", default: 0, null: false
+ t.float "repository_size", default: 0.0
+ t.integer "star_count", default: 0, null: false
t.string "import_type"
t.string "import_source"
- t.integer "commit_count", default: 0
+ t.integer "commit_count", default: 0
t.text "import_error"
t.integer "ci_id"
- t.boolean "builds_enabled", default: true, null: false
- t.boolean "shared_runners_enabled", default: true, null: false
+ t.boolean "builds_enabled", default: true, null: false
+ t.boolean "shared_runners_enabled", default: true, null: false
t.string "runners_token"
t.string "build_coverage_regex"
- t.boolean "build_allow_git_fetch", default: true, null: false
- t.integer "build_timeout", default: 3600, null: false
- t.boolean "pending_delete", default: false
- t.boolean "public_builds", default: true, null: false
+ t.boolean "build_allow_git_fetch", default: true, null: false
+ t.integer "build_timeout", default: 3600, null: false
+ t.boolean "pending_delete", default: false
+ t.boolean "public_builds", default: true, null: false
t.string "main_language"
- t.integer "pushes_since_gc", default: 0
+ t.integer "pushes_since_gc", default: 0
t.boolean "last_repository_check_failed"
t.datetime "last_repository_check_at"
end
@@ -818,6 +829,7 @@ ActiveRecord::Schema.define(version: 20160412140240) do
t.boolean "build_events", default: false, null: false
t.string "category", default: "common", null: false
t.boolean "default", default: false
+ t.boolean "wiki_page_events", default: true
end
add_index "services", ["category"], name: "index_services_on_category", using: :btree
@@ -1012,6 +1024,7 @@ ActiveRecord::Schema.define(version: 20160412140240) do
t.boolean "note_events", default: false, null: false
t.boolean "enable_ssl_verification", default: true
t.boolean "build_events", default: false, null: false
+ t.boolean "wiki_page_events", default: false, null: false
end
add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
diff --git a/doc/api/README.md b/doc/api/README.md
index 3a8fa6cebd1..ff039f1886f 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -33,6 +33,7 @@ following locations:
- [Build triggers](build_triggers.md)
- [Build Variables](build_variables.md)
- [Runners](runners.md)
+- [Licenses](licenses.md)
## Authentication
diff --git a/doc/api/licenses.md b/doc/api/licenses.md
new file mode 100644
index 00000000000..855b0eab56f
--- /dev/null
+++ b/doc/api/licenses.md
@@ -0,0 +1,147 @@
+# Licenses
+
+## List license templates
+
+Get all license templates.
+
+```
+GET /licenses
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `popular` | boolean | no | If passed, returns only popular licenses |
+
+```bash
+curl https://gitlab.example.com/api/v3/licenses?popular=1
+```
+
+Example response:
+
+```json
+[
+ {
+ "key": "apache-2.0",
+ "name": "Apache License 2.0",
+ "nickname": null,
+ "featured": true,
+ "html_url": "http://choosealicense.com/licenses/apache-2.0/",
+ "source_url": "http://www.apache.org/licenses/LICENSE-2.0.html",
+ "description": "A permissive license that also provides an express grant of patent rights from contributors to users.",
+ "conditions": [
+ "include-copyright",
+ "document-changes"
+ ],
+ "permissions": [
+ "commercial-use",
+ "modifications",
+ "distribution",
+ "patent-use",
+ "private-use"
+ ],
+ "limitations": [
+ "trademark-use",
+ "no-liability"
+ ],
+ "content": " Apache License\n Version 2.0, January 2004\n [...]"
+ },
+ {
+ "key": "gpl-3.0",
+ "name": "GNU General Public License v3.0",
+ "nickname": "GNU GPLv3",
+ "featured": true,
+ "html_url": "http://choosealicense.com/licenses/gpl-3.0/",
+ "source_url": "http://www.gnu.org/licenses/gpl-3.0.txt",
+ "description": "The GNU GPL is the most widely used free software license and has a strong copyleft requirement. When distributing derived works, the source code of the work must be made available under the same license.",
+ "conditions": [
+ "include-copyright",
+ "document-changes",
+ "disclose-source",
+ "same-license"
+ ],
+ "permissions": [
+ "commercial-use",
+ "modifications",
+ "distribution",
+ "patent-use",
+ "private-use"
+ ],
+ "limitations": [
+ "no-liability"
+ ],
+ "content": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n [...]"
+ },
+ {
+ "key": "mit",
+ "name": "MIT License",
+ "nickname": null,
+ "featured": true,
+ "html_url": "http://choosealicense.com/licenses/mit/",
+ "source_url": "http://opensource.org/licenses/MIT",
+ "description": "A permissive license that is short and to the point. It lets people do anything with your code with proper attribution and without warranty.",
+ "conditions": [
+ "include-copyright"
+ ],
+ "permissions": [
+ "commercial-use",
+ "modifications",
+ "distribution",
+ "private-use"
+ ],
+ "limitations": [
+ "no-liability"
+ ],
+ "content": "The MIT License (MIT)\n\nCopyright (c) [year] [fullname]\n [...]"
+ }
+]
+```
+
+## Single license template
+
+Get a single license template. You can pass parameters to replace the license
+placeholder.
+
+```
+GET /licenses/:key
+```
+
+| Attribute | Type | Required | Description |
+| ---------- | ------ | -------- | ----------- |
+| `key` | string | yes | The key of the license template |
+| `project` | string | no | The copyrighted project name |
+| `fullname` | string | no | The full-name of the copyright holder |
+
+>**Note:**
+If you omit the `fullname` parameter but authenticate your request, the name of
+the authenticated user will be used to replace the copyright holder placeholder.
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/licenses/mit?project=My+Cool+Project
+```
+
+Example response:
+
+```json
+{
+ "key": "mit",
+ "name": "MIT License",
+ "nickname": null,
+ "featured": true,
+ "html_url": "http://choosealicense.com/licenses/mit/",
+ "source_url": "http://opensource.org/licenses/MIT",
+ "description": "A permissive license that is short and to the point. It lets people do anything with your code with proper attribution and without warranty.",
+ "conditions": [
+ "include-copyright"
+ ],
+ "permissions": [
+ "commercial-use",
+ "modifications",
+ "distribution",
+ "private-use"
+ ],
+ "limitations": [
+ "no-liability"
+ ],
+ "content": "The MIT License (MIT)\n\nCopyright (c) 2016 John Doe\n [...]"
+}
+```
diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md
index d100e261178..79761a893da 100644
--- a/doc/ci/api/builds.md
+++ b/doc/ci/api/builds.md
@@ -26,48 +26,114 @@ This API uses two types of authentication:
### Runs oldest pending build by runner
- POST /ci/api/v1/builds/register
+```
+POST /ci/api/v1/builds/register
+```
-Parameters:
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `token` | string | yes | Unique runner token |
- * `token` (required) - Unique runner token
+```
+curl -X POST "https://gitlab.example.com/ci/api/v1/builds/register" -F "token=t0k3n"
+```
### Update details of an existing build
- PUT /ci/api/v1/builds/:id
+```
+PUT /ci/api/v1/builds/:id
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|----------------------|
+| `id` | integer | yes | The ID of a project |
+| `token` | string | yes | Unique runner token |
+| `state` | string | no | The state of a build |
+| `trace` | string | no | The trace of a build |
+
+```
+curl -X PUT "https://gitlab.example.com/ci/api/v1/builds/1234" -F "token=t0k3n" -F "state=running" -F "trace=Running git clone...\n"
+```
+
+### Incremental build trace update
+
+Using this method you need to send trace content as a request body. You also need to provide the `Content-Range` header
+with a range of sent trace part. Note that you need to send parts in the proper order, so the begining of the part
+must start just after the end of the previous part. If you provide the wrong part, then GitLab CI API will return `416
+Range Not Satisfiable` response with a header `Range: 0-X`, where `X` is the current trace length.
+
+For example, if you receive `Range: 0-11` in the response, then your next part must contain a `Content-Range: 11-...`
+header and a trace part covered by this range.
+
+For a valid update API will return `202` response with:
+* `Build-Status: {status}` header containing current status of the build,
+* `Range: 0-{length}` header with the current trace length.
+
+```
+PATCH /ci/api/v1/builds/:id/trace.txt
+```
Parameters:
- * `id` (required) - The ID of a project
- * `token` (required) - Unique runner token
- * `state` (optional) - The state of a build
- * `trace` (optional) - The trace of a build
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|----------------------|
+| `id` | integer | yes | The ID of a build |
+
+Headers:
+
+| Attribute | Type | Required | Description |
+|-----------------|---------|----------|-----------------------------------|
+| `BUILD-TOKEN` | string | yes | The build authorization token |
+| `Content-Range` | string | yes | Bytes range of trace that is sent |
+
+```
+curl -X PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" -H "BUILD-TOKEN=build_t0k3n" -H "Content-Range=0-21" -d "Running git clone...\n"
+```
+
### Upload artifacts to build
- POST /ci/api/v1/builds/:id/artifacts
+```
+POST /ci/api/v1/builds/:id/artifacts
+```
-Parameters:
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|-------------------------------|
+| `id` | integer | yes | The ID of a build |
+| `token` | string | yes | The build authorization token |
+| `file` | mixed | yes | Artifacts file |
- * `id` (required) - The ID of a build
- * `token` (required) - The build authorization token
- * `file` (required) - Artifacts file
+```
+curl -X POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" -F "file=@/path/to/file"
+```
### Download the artifacts file from build
- GET /ci/api/v1/builds/:id/artifacts
+```
+GET /ci/api/v1/builds/:id/artifacts
+```
-Parameters:
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|-------------------------------|
+| `id` | integer | yes | The ID of a build |
+| `token` | string | yes | The build authorization token |
- * `id` (required) - The ID of a build
- * `token` (required) - The build authorization token
+```
+curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
+```
### Remove the artifacts file from build
- DELETE /ci/api/v1/builds/:id/artifacts
+```
+DELETE /ci/api/v1/builds/:id/artifacts
+```
-Parameters:
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|-------------------------------|
+| ` id` | integer | yes | The ID of a build |
+| `token` | string | yes | The build authorization token |
- * ` id` (required) - The ID of a build
- * `token` (required) - The build authorization token
+```
+curl -X DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
+```
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index c7df0713a3d..a06650b3387 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -7,6 +7,10 @@ through the coordinator API of GitLab CI.
A runner can be specific to a certain project or serve any project
in GitLab CI. A runner that serves all projects is called a shared runner.
+Ideally, GitLab Runner should not be installed on the same machine as GitLab.
+Read the [requirements documentation](../../install/requirements.md#gitlab-runner)
+for more information.
+
## Shared vs. Specific Runners
A runner that is specific only runs for the specified project. A shared runner
@@ -140,7 +144,7 @@ to it. This means that if you have shared runners setup for a project and
someone forks that project, the shared runners will also serve jobs of this
project.
-# Attack vectors in runners
+## Attack vectors in Runners
Mentioned briefly earlier, but the following things of runners can be exploited.
We're always looking for contributions that can mitigate these [Security Considerations](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md).
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 61475b45988..7e9bced7616 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -15,6 +15,7 @@ If you want a quick introduction to GitLab CI, follow our
- [.gitlab-ci.yml](#gitlab-ci-yml)
- [image and services](#image-and-services)
- [before_script](#before_script)
+ - [after_script](#after_script)
- [stages](#stages)
- [types](#types)
- [variables](#variables)
@@ -30,6 +31,7 @@ If you want a quick introduction to GitLab CI, follow our
- [artifacts](#artifacts)
- [artifacts:name](#artifacts-name)
- [dependencies](#dependencies)
+ - [before_script and after_script](#before_script-and-after_script)
- [Hidden jobs](#hidden-jobs)
- [Special YAML features](#special-yaml-features)
- [Anchors](#anchors)
@@ -81,6 +83,9 @@ services:
before_script:
- bundle install
+after_script:
+ - rm secrets
+
stages:
- build
- test
@@ -105,6 +110,7 @@ There are a few reserved `keywords` that **cannot** be used as job names:
| stages | no | Define build stages |
| types | no | Alias for `stages` |
| before_script | no | Define commands that run before each job's script |
+| after_script | no | Define commands that run after each job's script |
| variables | no | Define build variables |
| cache | no | Define list of files that should be cached between subsequent runs |
@@ -119,6 +125,14 @@ used for time of the build. The configuration of this feature is covered in
`before_script` is used to define the command that should be run before all
builds, including deploy builds. This can be an array or a multi-line string.
+### after_script
+
+>**Note:**
+Introduced in GitLab 8.7 and GitLab Runner v1.2.
+
+`after_script` is used to define the command that will be run after for all
+builds. This has to be an array or a multi-line string.
+
### stages
`stages` is used to define build stages that can be used by jobs.
@@ -336,6 +350,8 @@ job_name:
| dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
| artifacts | no | Define list build artifacts |
| cache | no | Define list of files that should be cached between subsequent runs |
+| before_script | no | Override a set of commands that are executed before build |
+| after_script | no | Override a set of commands that are executed after build |
### script
@@ -692,6 +708,23 @@ deploy:
script: make deploy
```
+### before_script and after_script
+
+It's possible to overwrite globally defined `before_script` and `after_script`:
+
+```yaml
+before_script
+- global before script
+
+job:
+ before_script:
+ - execute this instead of global before script
+ script:
+ - my command
+ after_script:
+ - execute this after my script
+```
+
## Hidden jobs
>**Note:**
diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md
new file mode 100644
index 00000000000..3625c4191b8
--- /dev/null
+++ b/doc/downgrade_ee_to_ce/README.md
@@ -0,0 +1,82 @@
+# Downgrading from EE to CE
+
+If you ever decide to downgrade your Enterprise Edition back to the Community
+Edition, there are a few steps you need take before installing the CE package
+on top of the current EE package, or, if you are in an installation from source,
+before you change remotes and fetch the latest CE code.
+
+## Disable Enterprise-only features
+
+First thing to do is to disable the following features.
+
+### Authentication mechanisms
+
+Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so
+you should disable these mechanisms before downgrading and you should provide
+alternative authentication methods to your users.
+
+### Git Annex
+
+Git Annex is also only available on the Enterprise Edition. This means that if
+you have repositories that use Git Annex to store large files, these files will
+no longer be easily available via Git. You should consider migrating these
+repositories to use Git LFS before downgrading to the Community Edition.
+
+### Remove Jenkins CI Service entries from the database
+
+The `JenkinsService` class is only available on the Enterprise Edition codebase,
+so if you downgrade to the Community Edition, you'll come across the following
+error:
+
+```
+Completed 500 Internal Server Error in 497ms (ActiveRecord: 32.2ms)
+
+ActionView::Template::Error (The single-table inheritance mechanism failed to locate the subclass: 'JenkinsService'. This
+error is raised because the column 'type' is reserved for storing the class in case of inheritance. Please rename this
+column if you didn't intend it to be used for storing the inheritance class or overwrite Service.inheritance_column to
+use another column for that information.)
+```
+
+All services are created automatically for every project you have, so in order
+to avoid getting this error, you need to remove all instances of the
+`JenkinsService` from your database:
+
+**Omnibus Installation**
+
+```
+$ sudo gitlab-rails runner "Service.where(type: 'JenkinsService').delete_all"
+```
+
+**Source Installation**
+
+```
+$ bundle exec rails runner "Service.where(type: 'JenkinsService').delete_all" production
+```
+
+## Downgrade to CE
+
+After performing the above mentioned steps, you are now ready to downgrade your
+GitLab installation to the Community Edition.
+
+**Omnibus Installation**
+
+To downgrade an Omnibus installation, it is sufficient to install the Community
+Edition package on top of the currently installed one. You can do this manually,
+by directly [downloading the package](https://packages.gitlab.com/gitlab/gitlab-ce)
+you need, or by adding our CE package repository and following the
+[CE installation instructions](https://about.gitlab.com/downloads/).
+
+**Source Installation**
+
+To downgrade a source installation, you need to replace the current remote of
+your GitLab installation with the Community Edition's remote, fetch the latest
+changes, and checkout the latest stable branch:
+
+```
+$ git remote set-url origin git@gitlab.com:gitlab-org/gitlab-ce.git
+$ git fetch --all
+$ git checkout 8-x-stable
+```
+
+Remember to follow the correct [update guides](../update/README.md) to make
+sure all dependencies are up to date.
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 03cb08dd1f1..eb9fe5e1b1b 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -79,6 +79,26 @@ With less memory GitLab will give strange errors during the reconfigure run and
Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those.
+## Gitlab Runner
+
+We strongly advise against installing GitLab Runner on the same machine you plan
+to install GitLab on. Depending on how you decide to configure GitLab Runner and
+what tools you use to exercise your application in the CI environment, GitLab
+Runner can consume significant amount of available memory.
+
+Memory consumption calculations, that are available above, will not be valid if
+you decide to run GitLab Runner and the GitLab Rails application on the same
+machine.
+
+It is also not safe to install everything on a single machine, because of the
+[security reasons] - especially when you plan to use shell executor with GitLab
+Runner.
+
+We recommend using a separate machine for each GitLab Runner, if you plan to
+use the CI features.
+
+[security reasons]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md
+
## Unicorn Workers
It's possible to increase the amount of unicorn workers and this will usually help for to reduce the response time of the applications and increase the ability to handle parallel requests.
diff --git a/doc/intro/README.md b/doc/intro/README.md
index fecbbe6317b..ab298d3808e 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -25,6 +25,7 @@ Create merge requests and review code.
- [Automatically close issues from merge requests](../customization/issue_closing.md)
- [Automatically merge when your builds succeed](../workflow/merge_when_build_succeeds.md)
- [Revert any commit](../workflow/revert_changes.md)
+- [Cherry-pick any commit](../workflow/cherry_pick_changes.md)
## Test and Deploy
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index 612376e3a49..c44930a4ceb 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -4,6 +4,12 @@ Your GitLab instance can perform HTTP POST requests on the following events: `pr
System hooks can be used, e.g. for logging or changing information in a LDAP server.
+> **Note:**
+>
+> We follow the same structure from Webhooks for Push and Tag events, but we never display commits.
+>
+> Same deprecations from Webhooks are valid here.
+
## Hooks request example
**Request header**:
@@ -240,3 +246,110 @@ X-Gitlab-Event: System Hook
"user_id": 41
}
```
+
+## Push events
+
+Triggered when you push to the repository except when pushing tags.
+
+**Request header**:
+
+```
+X-Gitlab-Event: System Hook
+```
+
+**Request body:**
+
+```json
+{
+ "event_name": "push",
+ "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
+ "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "ref": "refs/heads/master",
+ "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "user_id": 4,
+ "user_name": "John Smith",
+ "user_email": "john@example.com",
+ "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
+ "project_id": 15,
+ "project":{
+ "name":"Diaspora",
+ "description":"",
+ "web_url":"http://example.com/mike/diaspora",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:mike/diaspora.git",
+ "git_http_url":"http://example.com/mike/diaspora.git",
+ "namespace":"Mike",
+ "visibility_level":0,
+ "path_with_namespace":"mike/diaspora",
+ "default_branch":"master",
+ "homepage":"http://example.com/mike/diaspora",
+ "url":"git@example.com:mike/diaspora.git",
+ "ssh_url":"git@example.com:mike/diaspora.git",
+ "http_url":"http://example.com/mike/diaspora.git"
+ },
+ "repository":{
+ "name": "Diaspora",
+ "url": "git@example.com:mike/diaspora.git",
+ "description": "",
+ "homepage": "http://example.com/mike/diaspora",
+ "git_http_url":"http://example.com/mike/diaspora.git",
+ "git_ssh_url":"git@example.com:mike/diaspora.git",
+ "visibility_level":0
+ },
+ "commits": [],
+ "total_commits_count": 0
+}
+```
+
+## Tag events
+
+Triggered when you create (or delete) tags to the repository.
+
+**Request header**:
+
+```
+X-Gitlab-Event: System Hook
+```
+
+**Request body:**
+
+```json
+{
+ "event_name": "tag_push",
+ "before": "0000000000000000000000000000000000000000",
+ "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
+ "ref": "refs/tags/v1.0.0",
+ "checkout_sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "user_id": 1,
+ "user_name": "John Smith",
+ "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
+ "project_id": 1,
+ "project":{
+ "name":"Example",
+ "description":"",
+ "web_url":"http://example.com/jsmith/example",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:jsmith/example.git",
+ "git_http_url":"http://example.com/jsmith/example.git",
+ "namespace":"Jsmith",
+ "visibility_level":0,
+ "path_with_namespace":"jsmith/example",
+ "default_branch":"master",
+ "homepage":"http://example.com/jsmith/example",
+ "url":"git@example.com:jsmith/example.git",
+ "ssh_url":"git@example.com:jsmith/example.git",
+ "http_url":"http://example.com/jsmith/example.git"
+ },
+ "repository":{
+ "name": "Example",
+ "url": "ssh://git@example.com/jsmith/example.git",
+ "description": "",
+ "homepage": "http://example.com/jsmith/example",
+ "git_http_url":"http://example.com/jsmith/example.git",
+ "git_ssh_url":"git@example.com:jsmith/example.git",
+ "visibility_level":0
+ },
+ "commits": [],
+ "total_commits_count": 0
+}
+```
diff --git a/doc/update/README.md b/doc/update/README.md
index 0241f036830..a770633c9b8 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -1,18 +1,95 @@
-Depending on the installation method and your GitLab version, there are multiple update guides. Choose one that fits your needs.
+# Updating GitLab
+
+Depending on the installation method and your GitLab version, there are multiple
+update guides.
+
+There are currently 3 official ways to install GitLab:
+
+- Omnibus packages
+- Source installation
+- Docker installation
+
+Based on your installation, choose a section below that fits your needs.
+
+---
+
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+
+- [Omnibus Packages](#omnibus-packages)
+- [Installation from source](#installation-from-source)
+- [Installation using Docker](#installation-using-docker)
+- [Upgrading between editions](#upgrading-between-editions)
+ - [Community to Enterprise Edition](#community-to-enterprise-edition)
+ - [Enterprise to Community Edition](#enterprise-to-community-edition)
+- [Miscellaneous](#miscellaneous)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Omnibus Packages
-- [Omnibus update guide](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/update.md) contains the steps needed to update a GitLab [package](https://about.gitlab.com/downloads/).
+- The [Omnibus update guide](http://doc.gitlab.com/omnibus/update/README.html)
+ contains the steps needed to update an Omnibus GitLab package.
## Installation from source
-- [The individual upgrade guides](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update) are for those who have installed GitLab from source.
-- [The CE to EE update guides](https://gitlab.com/subscribers/gitlab-ee/tree/master/doc/update) are for subscribers of the Enterprise Edition only. The steps are very similar to a version upgrade: stop the server, get the code, update config files for the new functionality, install libs and do migrations, update the init script, start the application and check the application status.
-- [Upgrader](upgrader.md) is an automatic ruby script that performs the update for installations from source.
-- [Patch versions](patch_versions.md) guide includes the steps needed for a patch version, eg. 6.2.0 to 6.2.1.
+- [Upgrading Community Edition from source][source-ce] - The individual
+ upgrade guides are for those who have installed GitLab CE from source.
+- [Upgrading Enterprise Edition from source][source-ee] - The individual
+ upgrade guides are for those who have installed GitLab EE from source.
+- [Patch versions](patch_versions.md) guide includes the steps needed for a
+ patch version, eg. 6.2.0 to 6.2.1, and apply to both Community and Enterprise
+ Editions.
+
+## Installation using Docker
+
+GitLab provides official Docker images for both Community and Enterprise
+editions. They are based on the Omnibus package and instructions on how to
+update them are in [a separate document][omnidocker].
+
+## Upgrading between editions
+
+GitLab comes in two flavors: [Community Edition][ce] which is MIT licensed,
+and [Enterprise Edition][ee] which builds on top of the Community Edition and
+includes extra features mainly aimed at organizations with more than 100 users.
+
+Below you can find some guides to help you change editions easily.
+
+### Community to Enterprise Edition
+
+>**Note:**
+The following guides are for subscribers of the Enterprise Edition only.
+
+If you wish to upgrade your GitLab installation from Community to Enterprise
+Edition, follow the guides below based on the installation method:
+
+- [Source CE to EE update guides][source-ee] - Find your version, and follow the
+ `-ce-to-ee.md` guide. The steps are very similar to a version upgrade: stop
+ the server, get the code, update config files for the new functionality,
+ install libraries and do migrations, update the init script, start the
+ application and check its status.
+- [Omnibus CE to EE][omni-ce-ee] - Follow this guide to update your Omnibus
+ GitLab Community Edition to the Enterprise Edition.
+
+### Enterprise to Community Edition
+
+If you need to downgrade your Enterprise Edition installation back to Community
+Edition, you can follow [this guide][ee-ce] to make the process as smooth as
+possible.
## Miscellaneous
-- [MySQL to PostgreSQL](mysql_to_postgresql.md) guides you through migrating your database from MySQL to PostgreSQL.
-- [MySQL installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/database_mysql.md) contains additional information about configuring GitLab to work with a MySQL database.
+- [MySQL to PostgreSQL](mysql_to_postgresql.md) guides you through migrating
+ your database from MySQL to PostgreSQL.
+- [MySQL installation guide](../install/database_mysql.md) contains additional
+ information about configuring GitLab to work with a MySQL database.
- [Restoring from backup after a failed upgrade](restore_after_failure.md)
+
+[omnidocker]: http://doc.gitlab.com/omnibus/docker/README.html
+[source-ee]: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc/update
+[source-ce]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update
+[ee-ce]: ../downgrade_ee_to_ce/README.md
+[ce]: https://about.gitlab.com/features/#community
+[ee]: https://about.gitlab.com/features/#enterprise
+[omni-ce-ee]: http://doc.gitlab.com/omnibus/update/README.html#from-community-edition-to-enterprise-edition
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index 22e207b6d32..c1c51302e79 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -41,6 +41,7 @@ X-Gitlab-Event: Push Hook
"before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
"after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"ref": "refs/heads/master",
+ "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"user_id": 4,
"user_name": "John Smith",
"user_email": "john@example.com",
@@ -118,9 +119,10 @@ X-Gitlab-Event: Tag Push Hook
```json
{
"object_kind": "tag_push",
- "ref": "refs/tags/v1.0.0",
"before": "0000000000000000000000000000000000000000",
"after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
+ "ref": "refs/tags/v1.0.0",
+ "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
"user_id": 1,
"user_name": "John Smith",
"user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 25893f948ea..9efe41308dc 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -20,6 +20,7 @@
- [Milestones](milestones.md)
- [Merge Requests](merge_requests.md)
- [Revert changes](revert_changes.md)
+- [Cherry-pick changes](cherry_pick_changes.md)
- ["Work In Progress" Merge Requests](wip_merge_requests.md)
- [Merge When Build Succeeds](merge_when_build_succeeds.md)
- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
diff --git a/doc/workflow/cherry_pick_changes.md b/doc/workflow/cherry_pick_changes.md
new file mode 100644
index 00000000000..b0ca0879643
--- /dev/null
+++ b/doc/workflow/cherry_pick_changes.md
@@ -0,0 +1,52 @@
+# Cherry-pick changes
+
+_**Note:** This feature was [introduced][ce-3514] in GitLab 8.7._
+
+---
+
+GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick]
+with introducing a **Cherry-pick** button in Merge Requests and commit details.
+
+## Cherry-picking a Merge Request
+
+After the Merge Request has been merged, a **Cherry-pick** button will be available
+to cherry-pick the changes introduced by that Merge Request:
+
+![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png)
+
+---
+
+You can cherry-pick the changes directly into the selected branch or you can opt to
+create a new Merge Request with the cherry-pick changes:
+
+![Cherry-pick Merge Request modal](img/cherry_pick_changes_mr_modal.png)
+
+## Cherry-picking a Commit
+
+You can cherry-pick a Commit from the Commit details page:
+
+![Cherry-pick commit](img/cherry_pick_changes_commit.png)
+
+---
+
+Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes
+directly into the target branch or create a new Merge Request to cherry-pick the
+changes:
+
+![Cherry-pick commit modal](img/cherry_pick_changes_commit_modal.png)
+
+---
+
+Please note that when cherry-picking merge commits, the mainline will always be the
+first parent. If you want to use a different mainline then you need to do that
+from the command line.
+
+Here is a quick example to cherry-pick a merge commit using the second parent as the
+mainline:
+
+```bash
+git cherry-pick -m 2 7a39eb0
+```
+
+[ce-3514]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3514 "Cherry-pick button Merge Request"
+[git-cherry-pick]: https://git-scm.com/docs/git-cherry-pick "Git cherry-pick documentation"
diff --git a/doc/workflow/img/cherry_pick_changes_commit.png b/doc/workflow/img/cherry_pick_changes_commit.png
new file mode 100644
index 00000000000..ae91d2cae53
--- /dev/null
+++ b/doc/workflow/img/cherry_pick_changes_commit.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_commit_modal.png b/doc/workflow/img/cherry_pick_changes_commit_modal.png
new file mode 100644
index 00000000000..f502f87677a
--- /dev/null
+++ b/doc/workflow/img/cherry_pick_changes_commit_modal.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_mr.png b/doc/workflow/img/cherry_pick_changes_mr.png
new file mode 100644
index 00000000000..59c610e620b
--- /dev/null
+++ b/doc/workflow/img/cherry_pick_changes_mr.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_mr_modal.png b/doc/workflow/img/cherry_pick_changes_mr_modal.png
new file mode 100644
index 00000000000..96a80f4726d
--- /dev/null
+++ b/doc/workflow/img/cherry_pick_changes_mr_modal.png
Binary files differ
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index ba91685a20b..31620044b15 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -44,7 +44,7 @@ check it into your Git repository:
```bash
git clone git@gitlab.example.com:group/project.git
-git lfs init # initialize the Git LFS project project
+git lfs install # initialize the Git LFS project project
git lfs track "*.iso" # select the file extensions that you want to treat as large files
```
diff --git a/features/project/issues/filter_labels.feature b/features/project/issues/filter_labels.feature
index e07f8053fb7..49d7a3b9af2 100644
--- a/features/project/issues/filter_labels.feature
+++ b/features/project/issues/filter_labels.feature
@@ -12,6 +12,7 @@ Feature: Project Issues Filter Labels
@javascript
Scenario: I filter by one label
Given I click link "bug"
+ And I click "dropdown close button"
Then I should see "Bugfix1" in issues list
And I should see "Bugfix2" in issues list
And I should not see "Feature1" in issues list
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index 1e09dbc4c8f..fdffd71de85 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -124,19 +124,6 @@ Feature: Project Source Browse Files
And I can see the replacement commit message
@javascript
- Scenario: I can create file in empty repo
- Given I own an empty project
- And I visit my empty project page
- And I create bare repo
- When I click on "add a file" link
- And I edit code
- And I fill the new file name
- And I fill the commit message
- And I click on "Commit Changes"
- Then I am redirected to the new file
- And I should see its new content
-
- @javascript
Scenario: If I enter an illegal file name I see an error message
Given I click on "New file" link in repo
And I fill the new file name with an illegal name
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index a6e574f12a9..2b23df6764b 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -31,7 +31,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
expect(page).to have_content 'Done 0'
expect(page).to have_link project.name_with_namespace
- should_see_todo(1, "John Doe assigned you merge request !#{merge_request.iid}", merge_request.title)
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title)
should_see_todo(2, "John Doe mentioned you on issue ##{issue.iid}", "#{current_user.to_reference} Wdyt?")
should_see_todo(3, "John Doe assigned you issue ##{issue.iid}", issue.title)
should_see_todo(4, "Mary Jane mentioned you on issue ##{issue.iid}", issue.title)
@@ -45,7 +45,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
page.within('.nav-sidebar') { expect(page).to have_content 'Todos 3' }
expect(page).to have_content 'To do 3'
expect(page).to have_content 'Done 1'
- should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}"
+ should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
end
step 'I click on the "Done" tab' do
@@ -54,7 +54,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
step 'I should see all todos marked as done' do
expect(page).to have_link project.name_with_namespace
- should_see_todo(1, "John Doe assigned you merge request !#{merge_request.iid}", merge_request.title, false)
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title, false)
end
step 'I filter by "Enterprise"' do
@@ -82,11 +82,11 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
end
step 'I should not see todos related to "Merge Requests" in the list' do
- should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}"
+ should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
end
step 'I should not see todos related to "Assignments" in the list' do
- should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}"
+ should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
should_not_see_todo "John Doe assigned you issue ##{issue.iid}"
end
diff --git a/features/steps/profile/active_tab.rb b/features/steps/profile/active_tab.rb
index 4724a326277..3b59089a093 100644
--- a/features/steps/profile/active_tab.rb
+++ b/features/steps/profile/active_tab.rb
@@ -22,4 +22,8 @@ class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps
step 'the active main tab should be Audit Log' do
ensure_active_main_tab('Audit Log')
end
+
+ def ensure_active_main_tab(content)
+ expect(find('.layout-nav li.active')).to have_content(content)
+ end
end
diff --git a/features/steps/project/commits/user_lookup.rb b/features/steps/project/commits/user_lookup.rb
index 40cada6da45..2d43be5a386 100644
--- a/features/steps/project/commits/user_lookup.rb
+++ b/features/steps/project/commits/user_lookup.rb
@@ -29,8 +29,9 @@ class Spinach::Features::ProjectCommitsUserLookup < Spinach::FeatureSteps
def check_author_link(email, user)
author_link = find('.commit-author-link')
+
expect(author_link['href']).to eq user_path(user)
- expect(author_link['data-original-title']).to eq email
+ expect(author_link['title']).to eq email
expect(find('.commit-author-name').text).to eq user.name
end
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index 612bb8fd8b1..0ead83d6937 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -114,7 +114,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
step 'I see the edit page prefilled for "Merge Request On Forked Project"' do
expect(current_path).to eq edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
- expect(page).to have_content "Edit merge request ##{@merge_request.id}"
+ expect(page).to have_content "Edit merge request #{@merge_request.to_reference}"
expect(find("#merge_request_title").value).to eq "Merge Request On Forked Project"
end
diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb
index 6d50501a722..d82c6856918 100644
--- a/features/steps/project/issues/filter_labels.rb
+++ b/features/steps/project/issues/filter_labels.rb
@@ -32,6 +32,10 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
page.find('.js-label-select').click
sleep 0.5
execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
+ end
+
+ step 'I click "dropdown close button"' do
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 4f883fe7c27..3b1a00f628a 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -519,7 +519,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step '"Bug NS-05" has CI status' do
project = merge_request.source_project
project.enable_ci
- ci_commit = create :ci_commit, project: project, sha: merge_request.last_commit.id
+ ci_commit = create :ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch
create :ci_build, commit: ci_commit
end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index e072505e5d7..c26d7a15212 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -282,8 +282,8 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
click_link 'Create empty bare repository'
end
- step 'I click on "add a file" link' do
- click_link 'adding README'
+ step 'I click on "README" link' do
+ click_link 'README'
# Remove pre-receive hook so we can push without auth
FileUtils.rm_f(File.join(@project.repository.path, 'hooks', 'pre-receive'))
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index c4c7672a432..cf30e23b6bd 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -10,16 +10,16 @@ module SharedBuilds
end
step 'project has a recent build' do
- @ci_commit = create(:ci_commit, project: @project, sha: @project.commit.sha)
+ @ci_commit = create(:ci_commit, project: @project, sha: @project.commit.sha, ref: 'master')
@build = create(:ci_build_with_coverage, commit: @ci_commit)
end
step 'recent build is successful' do
- @build.update_column(:status, 'success')
+ @build.update(status: 'success')
end
step 'recent build failed' do
- @build.update_column(:status, 'failed')
+ @build.update(status: 'failed')
end
step 'project has another build that is running' do
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index b13e82f276b..ea5f9580308 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -230,7 +230,7 @@ module SharedProject
step 'project "Shop" has CI build' do
project = Project.find_by(name: "Shop")
- create :ci_commit, project: project, sha: project.commit.sha
+ create :ci_commit, project: project, sha: project.commit.sha, ref: 'master', status: 'skipped'
end
step 'I should see last commit with CI status' do
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 7d65145176b..cc1004f8005 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -57,5 +57,6 @@ module API
mount Builds
mount Variables
mount Runners
+ mount Licenses
end
end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 8e74e177ea0..7388ed2f4ea 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -21,10 +21,9 @@ module API
authorize!(:read_commit_status, user_project)
not_found!('Commit') unless user_project.commit(params[:sha])
- ci_commit = user_project.ci_commit(params[:sha])
- return [] unless ci_commit
- statuses = ci_commit.statuses
+ ci_commits = user_project.ci_commits.where(sha: params[:sha])
+ statuses = ::CommitStatus.where(commit: ci_commits)
statuses = statuses.latest unless parse_boolean(params[:all])
statuses = statuses.where(ref: params[:ref]) if params[:ref].present?
statuses = statuses.where(stage: params[:stage]) if params[:stage].present?
@@ -51,7 +50,21 @@ module API
commit = @project.commit(params[:sha])
not_found! 'Commit' unless commit
- ci_commit = @project.ensure_ci_commit(commit.sha)
+ # Since the CommitStatus is attached to Ci::Commit (in the future Pipeline)
+ # We need to always have the pipeline object
+ # To have a valid pipeline object that can be attached to specific MR
+ # Other CI service needs to send `ref`
+ # If we don't receive it, we will attach the CommitStatus to
+ # the first found branch on that commit
+
+ ref = params[:ref]
+ unless ref
+ branches = @project.repository.branch_names_contains(commit.sha)
+ not_found! 'References for commit' if branches.none?
+ ref = branches.first
+ end
+
+ ci_commit = @project.ensure_ci_commit(commit.sha, ref)
name = params[:name] || params[:context]
status = GenericCommitStatus.running_or_pending.find_by(commit: ci_commit, name: name, ref: params[:ref])
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 60b9f5e0ece..716ca6f7ed9 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -439,5 +439,17 @@ module API
class Variable < Grape::Entity
expose :key, :value
end
+
+ class RepoLicense < Grape::Entity
+ expose :key, :name, :nickname
+ expose :featured, as: :popular
+ expose :url, as: :html_url
+ expose(:source_url) { |license| license.meta['source'] }
+ expose(:description) { |license| license.meta['description'] }
+ expose(:conditions) { |license| license.meta['conditions'] }
+ expose(:permissions) { |license| license.meta['permissions'] }
+ expose(:limitations) { |license| license.meta['limitations'] }
+ expose :content
+ end
end
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 8cfa1f1556b..3ac7b50c4ce 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -23,7 +23,7 @@ module API
end
post "/allowed" do
- Gitlab::Metrics.tag_transaction('action', 'Grape#/internal/allowed')
+ Gitlab::Metrics.action = 'Grape#/internal/allowed'
status 200
diff --git a/lib/api/licenses.rb b/lib/api/licenses.rb
new file mode 100644
index 00000000000..187d2c04703
--- /dev/null
+++ b/lib/api/licenses.rb
@@ -0,0 +1,58 @@
+module API
+ # Licenses API
+ class Licenses < Grape::API
+ PROJECT_TEMPLATE_REGEX =
+ /[\<\{\[]
+ (project|description|
+ one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
+ [\>\}\]]/xi.freeze
+ YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
+ FULLNAME_TEMPLATE_REGEX =
+ /[\<\{\[]
+ (fullname|name\sof\s(author|copyright\sowner))
+ [\>\}\]]/xi.freeze
+
+ # Get the list of the available license templates
+ #
+ # Parameters:
+ # popular - Filter licenses to only the popular ones
+ #
+ # Example Request:
+ # GET /licenses
+ # GET /licenses?popular=1
+ get 'licenses' do
+ options = {
+ featured: params[:popular].present? ? true : nil
+ }
+ present Licensee::License.all(options), with: Entities::RepoLicense
+ end
+
+ # Get text for specific license
+ #
+ # Parameters:
+ # key (required) - The key of a license
+ # project - Copyrighted project name
+ # fullname - Full name of copyright holder
+ #
+ # Example Request:
+ # GET /licenses/mit
+ #
+ get 'licenses/:key', requirements: { key: /[\w\.-]+/ } do
+ required_attributes! [:key]
+
+ not_found!('License') unless Licensee::License.find(params[:key])
+
+ # We create a fresh Licensee::License object since we'll modify its
+ # content in place below.
+ license = Licensee::License.new(params[:key])
+
+ license.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s)
+ license.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present?
+
+ fullname = params[:fullname].presence || current_user.try(:name)
+ license.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname
+
+ present license, with: Entities::RepoLicense
+ end
+ end
+end
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index d1a10479e44..3e1ed3fe5c7 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -12,7 +12,7 @@ module API
# Example Request:
# GET /projects/:id/repository/tags
get ":id/repository/tags" do
- present user_project.repo.tags.sort_by(&:name).reverse,
+ present user_project.repository.tags.sort_by(&:name).reverse,
with: Entities::RepoTag, project: user_project
end
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index a2987850d03..8488a493b55 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -18,9 +18,7 @@ module Banzai
def references_in(text, pattern = Label.reference_pattern)
text.gsub(pattern) do |match|
- project = project_from_ref($~[:project])
- params = label_params($~[:label_id].to_i, $~[:label_name])
- label = project.labels.find_by(params)
+ label = find_label($~[:project], $~[:label_id], $~[:label_name])
if label
yield match, label.id, $~[:project], $~
@@ -30,18 +28,12 @@ module Banzai
end
end
- def url_for_object(label, project)
- h = Gitlab::Routing.url_helpers
- h.namespace_project_issues_url(project.namespace, project, label_name: label.name,
- only_path: context[:only_path])
- end
+ def find_label(project_ref, label_id, label_name)
+ project = project_from_ref(project_ref)
+ return unless project
- def object_link_text(object, matches)
- if context[:project] == object.project
- LabelsHelper.render_colored_label(object)
- else
- LabelsHelper.render_colored_cross_project_label(object)
- end
+ label_params = label_params(label_id, label_name)
+ project.labels.find_by(label_params)
end
# Parameters to pass to `Label.find_by` based on the given arguments
@@ -55,7 +47,21 @@ module Banzai
if name
{ name: name.tr('"', '') }
else
- { id: id }
+ { id: id.to_i }
+ end
+ end
+
+ def url_for_object(label, project)
+ h = Gitlab::Routing.url_helpers
+ h.namespace_project_issues_url(project.namespace, project, label_name: label.name,
+ only_path: context[:only_path])
+ end
+
+ def object_link_text(object, matches)
+ if context[:project] == object.project
+ LabelsHelper.render_colored_label(object)
+ else
+ LabelsHelper.render_colored_cross_project_label(object)
end
end
end
diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb
index 4e85d2c3c74..353c4ddebf8 100644
--- a/lib/ci/api/api.rb
+++ b/lib/ci/api/api.rb
@@ -23,6 +23,8 @@ module Ci
rack_response({ 'message' => '500 Internal Server Error' }, 500)
end
+ content_type :txt, 'text/plain'
+ content_type :json, 'application/json'
format :json
helpers ::Ci::API::Helpers
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 2e9a5d311f9..607359769d1 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -50,6 +50,39 @@ module Ci
end
end
+ # Send incremental log update - Runners only
+ #
+ # Parameters:
+ # id (required) - The ID of a build
+ # Body:
+ # content of logs to append
+ # Headers:
+ # Content-Range (required) - range of content that was sent
+ # BUILD-TOKEN (required) - The build authorization token
+ # Example Request:
+ # PATCH /builds/:id/trace.txt
+ patch ":id/trace.txt" do
+ build = Ci::Build.find_by_id(params[:id])
+ not_found! unless build
+ authenticate_build_token!(build)
+ forbidden!('Build has been erased!') if build.erased?
+
+ error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
+ content_range = request.headers['Content-Range']
+ content_range = content_range.split('-')
+
+ current_length = build.trace_length
+ unless current_length == content_range[0].to_i
+ return error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{current_length}" })
+ end
+
+ build.append_trace(request.body.read, content_range[0].to_i)
+
+ status 202
+ header 'Build-Status', build.status
+ header 'Range', "0-#{build.trace_length}"
+ end
+
# Authorize artifacts uploading for build - Runners only
#
# Parameters:
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index b8ede3a7edc..ff9887cba1e 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -4,12 +4,12 @@ module Ci
DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test'
- ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables, :cache]
+ ALLOWED_YAML_KEYS = [:before_script, :after_script, :image, :services, :types, :stages, :variables, :cache]
ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services,
:allow_failure, :type, :stage, :when, :artifacts, :cache,
- :dependencies, :variables]
+ :dependencies, :before_script, :after_script, :variables]
- attr_reader :before_script, :image, :services, :path, :cache
+ attr_reader :before_script, :after_script, :image, :services, :path, :cache
def initialize(config, path = nil)
@config = YAML.safe_load(config, [Symbol], [], true)
@@ -55,6 +55,7 @@ module Ci
def initial_parsing
@before_script = @config[:before_script] || []
+ @after_script = @config[:after_script]
@image = @config[:image]
@services = @config[:services]
@stages = @config[:stages] || @config[:types]
@@ -83,7 +84,7 @@ module Ci
{
stage_idx: stages.index(job[:stage]),
stage: job[:stage],
- commands: "#{@before_script.join("\n")}\n#{normalize_script(job[:script])}",
+ commands: [job[:before_script] || @before_script, job[:script]].flatten.join("\n"),
tag_list: job[:tags] || [],
name: name,
only: job[:only],
@@ -96,23 +97,32 @@ module Ci
artifacts: job[:artifacts],
cache: job[:cache] || @cache,
dependencies: job[:dependencies],
+ after_script: job[:after_script] || @after_script,
}.compact
}
end
- def normalize_script(script)
- if script.is_a? Array
- script.join("\n")
- else
- script
+ def validate!
+ validate_global!
+
+ @jobs.each do |name, job|
+ validate_job!(name, job)
end
+
+ true
end
- def validate!
+ private
+
+ def validate_global!
unless validate_array_of_strings(@before_script)
raise ValidationError, "before_script should be an array of strings"
end
+ unless @after_script.nil? || validate_array_of_strings(@after_script)
+ raise ValidationError, "after_script should be an array of strings"
+ end
+
unless @image.nil? || @image.is_a?(String)
raise ValidationError, "image should be a string"
end
@@ -129,31 +139,28 @@ module Ci
raise ValidationError, "variables should be a map of key-value strings"
end
- if @cache
- if @cache[:key] && !validate_string(@cache[:key])
- raise ValidationError, "cache:key parameter should be a string"
- end
-
- if @cache[:untracked] && !validate_boolean(@cache[:untracked])
- raise ValidationError, "cache:untracked parameter should be an boolean"
- end
+ validate_global_cache! if @cache
+ end
- if @cache[:paths] && !validate_array_of_strings(@cache[:paths])
- raise ValidationError, "cache:paths parameter should be an array of strings"
- end
+ def validate_global_cache!
+ if @cache[:key] && !validate_string(@cache[:key])
+ raise ValidationError, "cache:key parameter should be a string"
end
- @jobs.each do |name, job|
- validate_job!(name, job)
+ if @cache[:untracked] && !validate_boolean(@cache[:untracked])
+ raise ValidationError, "cache:untracked parameter should be an boolean"
end
- true
+ if @cache[:paths] && !validate_array_of_strings(@cache[:paths])
+ raise ValidationError, "cache:paths parameter should be an array of strings"
+ end
end
def validate_job!(name, job)
validate_job_name!(name)
validate_job_keys!(name, job)
validate_job_types!(name, job)
+ validate_job_script!(name, job)
validate_job_stage!(name, job) if job[:stage]
validate_job_variables!(name, job) if job[:variables]
@@ -162,8 +169,6 @@ module Ci
validate_job_dependencies!(name, job) if job[:dependencies]
end
- private
-
def validate_job_name!(name)
if name.blank? || !validate_string(name)
raise ValidationError, "job name should be non-empty string"
@@ -179,10 +184,6 @@ module Ci
end
def validate_job_types!(name, job)
- if !validate_string(job[:script]) && !validate_array_of_strings(job[:script])
- raise ValidationError, "#{name} job: script should be a string or an array of a strings"
- end
-
if job[:image] && !validate_string(job[:image])
raise ValidationError, "#{name} job: image should be a string"
end
@@ -212,6 +213,20 @@ module Ci
end
end
+ def validate_job_script!(name, job)
+ if !validate_string(job[:script]) && !validate_array_of_strings(job[:script])
+ raise ValidationError, "#{name} job: script should be a string or an array of a strings"
+ end
+
+ if job[:before_script] && !validate_array_of_strings(job[:before_script])
+ raise ValidationError, "#{name} job: before_script should be an array of strings"
+ end
+
+ if job[:after_script] && !validate_array_of_strings(job[:after_script])
+ raise ValidationError, "#{name} job: after_script should be an array of strings"
+ end
+ end
+
def validate_job_stage!(name, job)
unless job[:stage].is_a?(String) && job[:stage].in?(stages)
raise ValidationError, "#{name} job: stage parameter should be #{stages.join(", ")}"
diff --git a/lib/ci/status.rb b/lib/ci/status.rb
deleted file mode 100644
index 3fb1fe29494..00000000000
--- a/lib/ci/status.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module Ci
- class Status
- def self.get_status(statuses)
- if statuses.none?
- 'skipped'
- elsif statuses.all? { |status| status.success? || status.ignored? }
- 'success'
- elsif statuses.all?(&:pending?)
- 'pending'
- elsif statuses.any?(&:running?) || statuses.any?(&:pending?)
- 'running'
- elsif statuses.all?(&:canceled?)
- 'canceled'
- else
- 'failed'
- end
- end
- end
-end
diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb
index 2eae55e534b..440dd44ece7 100644
--- a/lib/file_size_validator.rb
+++ b/lib/file_size_validator.rb
@@ -1,9 +1,9 @@
class FileSizeValidator < ActiveModel::EachValidator
- MESSAGES = { is: :wrong_size, minimum: :size_too_small, maximum: :size_too_big }.freeze
- CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
+ MESSAGES = { is: :wrong_size, minimum: :size_too_small, maximum: :size_too_big }.freeze
+ CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
- DEFAULT_TOKENIZER = lambda { |value| value.split(//) }
- RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long]
+ DEFAULT_TOKENIZER = -> (value) { value.split(//) }.freeze
+ RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long].freeze
def initialize(options)
if range = (options.delete(:in) || options.delete(:within))
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 7f70ab63f37..0f9e3ee14ee 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -16,7 +16,8 @@ module Gitlab
end
def execute
- import_labels && import_issues && import_pull_requests && import_wiki
+ import_labels && import_milestones && import_issues &&
+ import_pull_requests && import_wiki
end
private
@@ -35,6 +36,16 @@ module Gitlab
raise Projects::ImportService::Error, e.message
end
+ def import_milestones
+ client.list_milestones(project.import_source, state: :all).each do |raw_data|
+ Milestone.create!(MilestoneFormatter.new(project, raw_data).attributes)
+ end
+
+ true
+ rescue ActiveRecord::RecordInvalid => e
+ raise Projects::ImportService::Error, e.message
+ end
+
def import_issues
client.list_issues(project.import_source, state: :all,
sort: :created,
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
index acb332cb0cb..c8173913b4e 100644
--- a/lib/gitlab/github_import/issue_formatter.rb
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -5,6 +5,7 @@ module Gitlab
{
iid: number,
project: project,
+ milestone: milestone,
title: raw_data.title,
description: description,
state: state,
@@ -55,6 +56,12 @@ module Gitlab
@formatter.author_line(author) + body
end
+ def milestone
+ if raw_data.milestone.present?
+ project.milestones.find_by(iid: raw_data.milestone.number)
+ end
+ end
+
def state
raw_data.state == 'closed' ? 'closed' : 'opened'
end
diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb
new file mode 100644
index 00000000000..e91a7e328cf
--- /dev/null
+++ b/lib/gitlab/github_import/milestone_formatter.rb
@@ -0,0 +1,48 @@
+module Gitlab
+ module GithubImport
+ class MilestoneFormatter < BaseFormatter
+ def attributes
+ {
+ iid: number,
+ project: project,
+ title: title,
+ description: description,
+ due_date: due_date,
+ state: state,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+ end
+
+ private
+
+ def number
+ raw_data.number
+ end
+
+ def title
+ raw_data.title
+ end
+
+ def description
+ raw_data.description
+ end
+
+ def due_date
+ raw_data.due_on
+ end
+
+ def state
+ raw_data.state == 'closed' ? 'closed' : 'active'
+ end
+
+ def created_at
+ raw_data.created_at
+ end
+
+ def updated_at
+ state == 'closed' ? raw_data.closed_at : raw_data.updated_at
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index 5ee8a14624a..d21b942ad4b 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -11,6 +11,7 @@ module Gitlab
target_project: target_project,
target_branch: target_branch.name,
state: state,
+ milestone: milestone,
author_id: author_id,
assignee_id: assignee_id,
created_at: raw_data.created_at,
@@ -58,6 +59,12 @@ module Gitlab
formatter.author_line(author) + body
end
+ def milestone
+ if raw_data.milestone.present?
+ project.milestones.find_by(iid: raw_data.milestone.number)
+ end
+ end
+
def source_project
project
end
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index 484970c5a10..49f702f91f6 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -14,7 +14,8 @@ module Gitlab
method_call_threshold: current_application_settings[:metrics_method_call_threshold],
host: current_application_settings[:metrics_host],
port: current_application_settings[:metrics_port],
- sample_interval: current_application_settings[:metrics_sample_interval] || 15
+ sample_interval: current_application_settings[:metrics_sample_interval] || 15,
+ packet_size: current_application_settings[:metrics_packet_size] || 1
}
end
@@ -41,9 +42,9 @@ module Gitlab
prepared = prepare_metrics(metrics)
pool.with do |connection|
- prepared.each do |metric|
+ prepared.each_slice(settings[:packet_size]) do |slice|
begin
- connection.write_points([metric])
+ connection.write_points(slice)
rescue StandardError
end
end
@@ -114,6 +115,15 @@ module Gitlab
trans.add_tag(name, value) if trans
end
+ # Sets the action of the current transaction (if any)
+ #
+ # action - The name of the action.
+ def self.action=(action)
+ trans = current_transaction
+
+ trans.action = action if trans
+ end
+
# When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe.
if enabled?
diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb
index 97d1edab9c1..67622f321a6 100644
--- a/lib/gitlab/push_data_builder.rb
+++ b/lib/gitlab/push_data_builder.rb
@@ -36,11 +36,12 @@ module Gitlab
commit.hook_attrs(with_changed_files: true)
end
- type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push"
+ type = Gitlab::Git.tag_ref?(ref) ? 'tag_push' : 'push'
# Hash to be passed as post_receive_data
data = {
object_kind: type,
+ event_name: type,
before: oldrev,
after: newrev,
ref: ref,
diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb
index 37232743325..ae85b294d31 100644
--- a/lib/gitlab/sidekiq_middleware/memory_killer.rb
+++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb
@@ -29,8 +29,8 @@ module Gitlab
"in #{GRACE_TIME} seconds"
sleep(GRACE_TIME)
- Sidekiq.logger.warn "sending SIGUSR1 to PID #{Process.pid}"
- Process.kill('SIGUSR1', Process.pid)
+ Sidekiq.logger.warn "sending SIGTERM to PID #{Process.pid}"
+ Process.kill('SIGTERM', Process.pid)
Sidekiq.logger.warn "waiting #{SHUTDOWN_WAIT} seconds before sending "\
"#{SHUTDOWN_SIGNAL} to PID #{Process.pid}"
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index f1943222edf..2bbbd3074e8 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -20,6 +20,8 @@ module Gitlab
merge_request_url(object)
when Note
note_url
+ when WikiPage
+ wiki_page_url
else
raise NotImplementedError.new("No URL builder defined for #{object.class}")
end
@@ -58,5 +60,9 @@ module Gitlab
project_snippet_url(snippet, anchor: dom_id(object))
end
end
+
+ def wiki_page_url
+ "#{Gitlab.config.gitlab.url}#{object.wiki.wiki_base_path}/#{object.slug}"
+ end
end
end
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 4a7ee7dbb64..247383aa46c 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -11,7 +11,7 @@ retry() {
return 1
}
-if [ -f /.dockerinit ]; then
+if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
mkdir -p vendor
# Install phantomjs package
diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb
index f09e4fcb154..cf5c606c723 100644
--- a/spec/controllers/commit_controller_spec.rb
+++ b/spec/controllers/commit_controller_spec.rb
@@ -4,6 +4,8 @@ describe Projects::CommitController do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:commit) { project.commit("master") }
+ let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
+ let(:master_pickable_commit) { project.commit(master_pickable_sha) }
before do
sign_in(user)
@@ -192,4 +194,53 @@ describe Projects::CommitController do
end
end
end
+
+ describe '#cherry_pick' do
+ context 'when target branch is not provided' do
+ it 'should render the 404 page' do
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: master_pickable_commit.id)
+
+ expect(response).not_to be_success
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when the cherry-pick was successful' do
+ it 'should redirect to the commits page' do
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: master_pickable_commit.id)
+
+ expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
+ expect(flash[:notice]).to eq('The commit has been successfully cherry-picked.')
+ end
+ end
+
+ context 'when the cherry_pick failed' do
+ before do
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: master_pickable_commit.id)
+ end
+
+ it 'should redirect to the commit page' do
+ # Cherry-picking a commit that has been already cherry-picked.
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: master_pickable_commit.id)
+
+ expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
+ expect(flash[:alert]).to match('Sorry, we cannot cherry-pick this commit automatically.')
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
new file mode 100644
index 00000000000..40bd83af861
--- /dev/null
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Projects::GroupLinksController do
+ let(:project) { create(:project, :private) }
+ let(:group) { create(:group, :private) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe '#create' do
+ shared_context 'link project to group' do
+ before do
+ post(:create, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ link_group_id: group.id,
+ link_group_access: ProjectGroupLink.default_access)
+ end
+ end
+
+ context 'when user has access to group he want to link project to' do
+ before { group.add_developer(user) }
+ include_context 'link project to group'
+
+ it 'links project with selected group' do
+ expect(group.shared_projects).to include project
+ end
+
+ it 'redirects to project group links page'do
+ expect(response).to redirect_to(
+ namespace_project_group_links_path(project.namespace, project)
+ )
+ end
+ end
+
+ context 'when user doers not have access to group he want to link to' do
+ include_context 'link project to group'
+
+ it 'renders 404' do
+ expect(response.status).to eq 404
+ end
+
+ it 'does not share project with that group' do
+ expect(group.shared_projects).to_not include project
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index d47e4ab9a4f..ed64e7cf9af 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -46,4 +46,20 @@ describe Projects::ProjectMembersController do
end
end
end
+
+ describe '#index' do
+ let(:project) { create(:project, :private) }
+
+ context 'when user is member' do
+ let(:member) { create(:user) }
+
+ before do
+ project.team << [member, :guest]
+ sign_in(member)
+ get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+ end
+
+ it { expect(response.status).to eq(200) }
+ end
+ end
end
diff --git a/spec/factories/project_wikis.rb b/spec/factories/project_wikis.rb
new file mode 100644
index 00000000000..a3403fd76ae
--- /dev/null
+++ b/spec/factories/project_wikis.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :project_wiki do
+ project factory: :empty_project
+ user factory: :user
+ initialize_with { new(project, user) }
+ end
+end
diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb
new file mode 100644
index 00000000000..938ccf2306b
--- /dev/null
+++ b/spec/factories/wiki_pages.rb
@@ -0,0 +1,9 @@
+require 'ostruct'
+
+FactoryGirl.define do
+ factory :wiki_page do
+ page = OpenStruct.new(url_path: 'some-name')
+ association :wiki, factory: :project_wiki, strategy: :build
+ initialize_with { new(wiki, page, true) }
+ end
+end
diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb
index 6da3a857b3f..090a941958f 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/builds_spec.rb
@@ -86,6 +86,20 @@ describe "Builds" do
end
end
end
+
+ context 'Build raw trace' do
+ before do
+ @build.run!
+ @build.trace = 'BUILD TRACE'
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ it do
+ page.within('.build-controls') do
+ expect(page).to have_link 'Raw'
+ end
+ end
+ end
end
describe "POST /:project/builds/:id/cancel" do
@@ -120,4 +134,20 @@ describe "Builds" do
it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) }
end
+
+ describe "GET /:project/builds/:id/raw" do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ @build.trace = 'BUILD TRACE'
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ it 'sends the right headers' do
+ page.within('.build-controls') { click_link 'Raw' }
+
+ expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
+ end
+ end
end
diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb
new file mode 100644
index 00000000000..7f654684143
--- /dev/null
+++ b/spec/features/issues/filter_by_labels_spec.rb
@@ -0,0 +1,167 @@
+require 'rails_helper'
+
+feature 'Issue filtering by Labels', feature: true do
+ include WaitForAjax
+
+ let(:project) { create(:project, :public) }
+ let!(:user) { create(:user)}
+ let!(:label) { create(:label, project: project) }
+
+ before do
+ bug = create(:label, project: project, title: 'bug')
+ feature = create(:label, project: project, title: 'feature')
+ enhancement = create(:label, project: project, title: 'enhancement')
+
+ issue1 = create(:issue, title: "Bugfix1", project: project)
+ issue1.labels << bug
+
+ issue2 = create(:issue, title: "Bugfix2", project: project)
+ issue2.labels << bug
+ issue2.labels << enhancement
+
+ issue3 = create(:issue, title: "Feature1", project: project)
+ issue3.labels << feature
+
+ project.team << [user, :master]
+ login_as(user)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ context 'filter by label bug', js: true do
+ before do
+ page.find('.js-label-select').click
+ wait_for_ajax
+ execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+ wait_for_ajax
+ end
+
+ it 'should show issue "Bugfix1" and "Bugfix2" in issues list' do
+ expect(page).to have_content "Bugfix1"
+ expect(page).to have_content "Bugfix2"
+ end
+
+ it 'should not show "Feature1" in issues list' do
+ expect(page).not_to have_content "Feature1"
+ end
+
+ it 'should show label "bug" in filtered-labels' do
+ expect(find('.filtered-labels')).to have_content "bug"
+ end
+
+ it 'should not show label "feature" and "enhancement" in filtered-labels' do
+ expect(find('.filtered-labels')).not_to have_content "feature"
+ expect(find('.filtered-labels')).not_to have_content "enhancement"
+ end
+ end
+
+ context 'filter by label feature', js: true do
+ before do
+ page.find('.js-label-select').click
+ wait_for_ajax
+ execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()")
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+ wait_for_ajax
+ end
+
+ it 'should show issue "Feature1" in issues list' do
+ expect(page).to have_content "Feature1"
+ end
+
+ it 'should not show "Bugfix1" and "Bugfix2" in issues list' do
+ expect(page).not_to have_content "Bugfix2"
+ expect(page).not_to have_content "Bugfix1"
+ end
+
+ it 'should show label "feature" in filtered-labels' do
+ expect(find('.filtered-labels')).to have_content "feature"
+ end
+
+ it 'should not show label "bug" and "enhancement" in filtered-labels' do
+ expect(find('.filtered-labels')).not_to have_content "bug"
+ expect(find('.filtered-labels')).not_to have_content "enhancement"
+ end
+ end
+
+ context 'filter by label enhancement', js: true do
+ before do
+ page.find('.js-label-select').click
+ wait_for_ajax
+ execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+ wait_for_ajax
+ end
+
+ it 'should show issue "Bugfix2" in issues list' do
+ expect(page).to have_content "Bugfix2"
+ end
+
+ it 'should not show "Feature1" and "Bugfix1" in issues list' do
+ expect(page).not_to have_content "Feature1"
+ expect(page).not_to have_content "Bugfix1"
+ end
+
+ it 'should show label "enhancement" in filtered-labels' do
+ expect(find('.filtered-labels')).to have_content "enhancement"
+ end
+
+ it 'should not show label "feature" and "bug" in filtered-labels' do
+ expect(find('.filtered-labels')).not_to have_content "bug"
+ expect(find('.filtered-labels')).not_to have_content "feature"
+ end
+ end
+
+ context 'filter by label enhancement or feature', js: true do
+ before do
+ page.find('.js-label-select').click
+ wait_for_ajax
+ execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
+ execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()")
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+ wait_for_ajax
+ end
+
+ it 'should not show "Bugfix1" or "Feature1" in issues list' do
+ expect(page).not_to have_content "Bugfix1"
+ expect(page).not_to have_content "Feature1"
+ end
+
+ it 'should show label "enhancement" and "feature" in filtered-labels' do
+ expect(find('.filtered-labels')).to have_content "enhancement"
+ expect(find('.filtered-labels')).to have_content "feature"
+ end
+
+ it 'should not show label "bug" in filtered-labels' do
+ expect(find('.filtered-labels')).not_to have_content "bug"
+ end
+ end
+
+ context 'filter by label enhancement and bug in issues list', js: true do
+ before do
+ page.find('.js-label-select').click
+ wait_for_ajax
+ execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
+ execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+ wait_for_ajax
+ end
+
+ it 'should show issue "Bugfix2" in issues list' do
+ expect(page).to have_content "Bugfix2"
+ end
+
+ it 'should not show "Feature1"' do
+ expect(page).not_to have_content "Feature1"
+ end
+
+ it 'should show label "bug" and "enhancement" in filtered-labels' do
+ expect(find('.filtered-labels')).to have_content "bug"
+ expect(find('.filtered-labels')).to have_content "enhancement"
+ end
+
+ it 'should not show label "feature" in filtered-labels' do
+ expect(find('.filtered-labels')).not_to have_content "feature"
+ end
+ end
+end
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index 69b22232f10..192e3619375 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -84,14 +84,20 @@ describe 'Filter issues', feature: true do
it 'should filter by any label' do
find('.dropdown-menu-labels a', text: 'Any Label').click
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+ sleep 2
+
page.within '.labels-filter' do
expect(page).to have_content 'Any Label'
end
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Label')
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Any Label')
end
it 'should filter by no label' do
find('.dropdown-menu-labels a', text: 'No Label').click
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+ sleep 2
+
page.within '.labels-filter' do
expect(page).to have_content 'No Label'
end
@@ -121,6 +127,7 @@ describe 'Filter issues', feature: true do
find('.js-label-select').click
find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2
end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
new file mode 100644
index 00000000000..5739bc64dfb
--- /dev/null
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -0,0 +1,79 @@
+require 'rails_helper'
+
+feature 'Issue Sidebar', feature: true do
+ let(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project) }
+ let!(:user) { create(:user)}
+
+ before do
+ create(:label, project: project, title: 'bug')
+ login_as(user)
+ end
+
+ context 'as a allowed user' do
+ before do
+ project.team << [user, :developer]
+ visit_issue(project, issue)
+ end
+
+ describe 'when clicking on edit labels', js: true do
+ it 'dropdown has an option to create a new label' do
+ find('.block.labels .edit-link').click
+
+ page.within('.block.labels') do
+ expect(page).to have_content 'Create new'
+ end
+ end
+ end
+
+ context 'creating a new label', js: true do
+ it 'option to crate a new label is present' do
+ page.within('.block.labels') do
+ find('.edit-link').click
+
+ expect(page).to have_content 'Create new'
+ end
+ end
+
+ it 'dropdown switches to "create label" section' do
+ page.within('.block.labels') do
+ find('.edit-link').click
+ click_link 'Create new'
+
+ expect(page).to have_content 'Create new label'
+ end
+ end
+
+ it 'new label is added' do
+ page.within('.block.labels') do
+ find('.edit-link').click
+ sleep 1
+ click_link 'Create new'
+
+ fill_in 'new_label_name', with: 'wontfix'
+ page.find(".suggest-colors a", match: :first).click
+ click_button 'Create'
+
+ page.within('.dropdown-page-one') do
+ expect(page).to have_content 'wontfix'
+ end
+ end
+ end
+ end
+ end
+
+ context 'as a guest' do
+ before do
+ project.team << [user, :guest]
+ visit_issue(project, issue)
+ end
+
+ it 'does not have a option to edit labels' do
+ expect(page).not_to have_selector('.block.labels .edit-link')
+ end
+ end
+
+ def visit_issue(project, issue)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+end
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index 3eb903a93fe..b03dd0f666d 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -48,7 +48,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
click_update_issues_button
page.within('.issue .controls') do
- expect(find('.author_link')["data-original-title"]).to have_content(user.name)
+ expect(find('.author_link')["title"]).to have_content(user.name)
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 35c8f93abc1..90476ab369b 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -112,7 +112,7 @@ describe 'Issues', feature: true do
end
describe 'filter issue' do
- titles = ['foo','bar','baz']
+ titles = %w[foo bar baz]
titles.each_with_index do |title, index|
let!(title.to_sym) do
create(:issue, title: title,
@@ -153,8 +153,94 @@ describe 'Issues', feature: true do
expect(first_issue).to include('baz')
end
+ describe 'sorting by due date' do
+ before do
+ foo.update(due_date: 1.day.from_now)
+ bar.update(due_date: 6.days.from_now)
+ end
+
+ it 'sorts by recently due date' do
+ visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_soon)
+
+ expect(first_issue).to include('foo')
+ end
+
+ it 'sorts by least recently due date' do
+ visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_later)
+
+ expect(first_issue).to include('bar')
+ end
+
+ it 'sorts by least recently due date by excluding nil due dates' do
+ bar.update(due_date: nil)
+
+ visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_later)
+
+ expect(first_issue).to include('foo')
+ end
+ end
+
+ describe 'filtering by due date' do
+ before do
+ foo.update(due_date: 1.day.from_now)
+ bar.update(due_date: 6.days.from_now)
+ end
+
+ it 'filters by none' do
+ visit namespace_project_issues_path(project.namespace, project, due_date: Issue::NoDueDate.name)
+
+ expect(page).not_to have_content('foo')
+ expect(page).not_to have_content('bar')
+ expect(page).to have_content('baz')
+ end
+
+ it 'filters by any' do
+ visit namespace_project_issues_path(project.namespace, project, due_date: Issue::AnyDueDate.name)
+
+ expect(page).to have_content('foo')
+ expect(page).to have_content('bar')
+ expect(page).to have_content('baz')
+ end
+
+ it 'filters by due this week' do
+ foo.update(due_date: Date.today.beginning_of_week + 2.days)
+ bar.update(due_date: Date.today.end_of_week)
+ baz.update(due_date: Date.today - 8.days)
+
+ visit namespace_project_issues_path(project.namespace, project, due_date: Issue::DueThisWeek.name)
+
+ expect(page).to have_content('foo')
+ expect(page).to have_content('bar')
+ expect(page).not_to have_content('baz')
+ end
+
+ it 'filters by due this month' do
+ foo.update(due_date: Date.today.beginning_of_month + 2.days)
+ bar.update(due_date: Date.today.end_of_month)
+ baz.update(due_date: Date.today - 50.days)
+
+ visit namespace_project_issues_path(project.namespace, project, due_date: Issue::DueThisMonth.name)
+
+ expect(page).to have_content('foo')
+ expect(page).to have_content('bar')
+ expect(page).not_to have_content('baz')
+ end
+
+ it 'filters by overdue' do
+ foo.update(due_date: Date.today + 2.days)
+ bar.update(due_date: Date.today + 20.days)
+ baz.update(due_date: Date.yesterday)
+
+ visit namespace_project_issues_path(project.namespace, project, due_date: Issue::Overdue.name)
+
+ expect(page).not_to have_content('foo')
+ expect(page).not_to have_content('bar')
+ expect(page).to have_content('baz')
+ end
+ end
+
describe 'sorting by milestone' do
- before :each do
+ before do
foo.milestone = newer_due_milestone
foo.save
bar.milestone = later_due_milestone
@@ -177,7 +263,7 @@ describe 'Issues', feature: true do
describe 'combine filter and sort' do
let(:user2) { create(:user) }
- before :each do
+ before do
foo.assignee = user2
foo.save
bar.assignee = user2
@@ -224,7 +310,7 @@ describe 'Issues', feature: true do
let(:guest) { create(:user) }
- before :each do
+ before do
project.team << [[guest], :guest]
end
@@ -267,7 +353,7 @@ describe 'Issues', feature: true do
context 'by unauthorized user' do
let(:guest) { create(:user) }
- before :each do
+ before do
project.team << [guest, :guest]
issue.milestone = milestone
issue.save
@@ -285,7 +371,7 @@ describe 'Issues', feature: true do
describe 'removing assignee' do
let(:user2) { create(:user) }
- before :each do
+ before do
issue.assignee = user2
issue.save
end
diff --git a/spec/features/merge_requests/cherry_pick_spec.rb b/spec/features/merge_requests/cherry_pick_spec.rb
new file mode 100644
index 00000000000..82bc5226d07
--- /dev/null
+++ b/spec/features/merge_requests/cherry_pick_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'Cherry-pick Merge Requests' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user) }
+
+ before do
+ login_as user
+ project.team << [user, :master]
+ end
+
+ context "Viewing a merged merge request" do
+ before do
+ service = MergeRequests::MergeService.new(project, user)
+
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ end
+ end
+
+ # Fast-forward merge, or merged before GitLab 8.5.
+ context "Without a merge commit" do
+ before do
+ merge_request.merge_commit_sha = nil
+ merge_request.save
+ end
+
+ it "doesn't show a Cherry-pick button" do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ expect(page).not_to have_link "Cherry-pick"
+ end
+ end
+
+ context "With a merge commit" do
+ it "shows a Cherry-pick button" do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ expect(page).to have_link "Cherry-pick"
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index c57ab5f3b03..e3ecd60a5f3 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -2,8 +2,14 @@ require 'rails_helper'
feature 'Merge Request filtering by Milestone', feature: true do
let(:project) { create(:project, :public) }
+ let!(:user) { create(:user)}
let(:milestone) { create(:milestone, project: project) }
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
scenario 'filters by no Milestone', js: true do
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
diff --git a/spec/features/project/commits/cherry_pick_spec.rb b/spec/features/project/commits/cherry_pick_spec.rb
new file mode 100644
index 00000000000..0559b02f321
--- /dev/null
+++ b/spec/features/project/commits/cherry_pick_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe 'Cherry-pick Commits' do
+ let(:project) { create(:project) }
+ let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
+ let(:master_pickable_merge) { project.commit('e56497bb5f03a90a51293fc6d516788730953899') }
+
+
+ before do
+ login_as :user
+ project.team << [@user, :master]
+ visit namespace_project_commits_path(project.namespace, project, project.repository.root_ref, { limit: 5 })
+ end
+
+ context "I cherry-pick a commit" do
+ it do
+ visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
+ find("a[href='#modal-cherry-pick-commit']").click
+ page.within('#modal-cherry-pick-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Cherry-pick'
+ end
+ expect(page).to have_content('The commit has been successfully cherry-picked.')
+ end
+ end
+
+ context "I cherry-pick a merge commit" do
+ it do
+ visit namespace_project_commit_path(project.namespace, project, master_pickable_merge.id)
+ find("a[href='#modal-cherry-pick-commit']").click
+ page.within('#modal-cherry-pick-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Cherry-pick'
+ end
+ expect(page).to have_content('The commit has been successfully cherry-picked.')
+ end
+ end
+
+ context "I cherry-pick a commit that was previously cherry-picked" do
+ it do
+ visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
+ find("a[href='#modal-cherry-pick-commit']").click
+ page.within('#modal-cherry-pick-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Cherry-pick'
+ end
+ visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
+ find("a[href='#modal-cherry-pick-commit']").click
+ page.within('#modal-cherry-pick-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Cherry-pick'
+ end
+ expect(page).to have_content('Sorry, we cannot cherry-pick this commit automatically.')
+ end
+ end
+
+ context "I cherry-pick a commit in a new merge request" do
+ it do
+ visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
+ find("a[href='#modal-cherry-pick-commit']").click
+ page.within('#modal-cherry-pick-commit') do
+ click_button 'Cherry-pick'
+ end
+ expect(page).to have_content('The commit has been successfully cherry-picked. You can now submit a merge request to get this change into the original branch.')
+ end
+ end
+end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
new file mode 100644
index 00000000000..3d6ffbc4c6b
--- /dev/null
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+feature 'project owner creates a license file', feature: true, js: true do
+ include Select2Helper
+
+ let(:project_master) { create(:user) }
+ let(:project) { create(:project) }
+ background do
+ project.repository.remove_file(project_master, 'LICENSE', 'Remove LICENSE', 'master')
+ project.team << [project_master, :master]
+ login_as(project_master)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'project master creates a license file manually from a template' do
+ visit namespace_project_tree_path(project.namespace, project, project.repository.root_ref)
+ find('.add-to-tree').click
+ click_link 'New file'
+
+ fill_in :file_name, with: 'LICENSE'
+
+ expect(page).to have_selector('.license-selector')
+
+ select2('mit', from: '#license_type')
+
+ file_content = find('.file-content')
+ expect(file_content).to have_content('The MIT License (MIT)')
+ expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+
+ fill_in :commit_message, with: 'Add a LICENSE file', visible: true
+ click_button 'Commit Changes'
+
+ expect(current_path).to eq(
+ namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
+ expect(page).to have_content('The MIT License (MIT)')
+ expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ end
+
+ scenario 'project master creates a license file from the "Add license" link' do
+ click_link 'Add License'
+
+ expect(current_path).to eq(
+ namespace_project_new_blob_path(project.namespace, project, 'master'))
+ expect(find('#file_name').value).to eq('LICENSE')
+ expect(page).to have_selector('.license-selector')
+
+ select2('mit', from: '#license_type')
+
+ file_content = find('.file-content')
+ expect(file_content).to have_content('The MIT License (MIT)')
+ expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+
+ fill_in :commit_message, with: 'Add a LICENSE file', visible: true
+ click_button 'Commit Changes'
+
+ expect(current_path).to eq(
+ namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
+ expect(page).to have_content('The MIT License (MIT)')
+ expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ end
+end
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
new file mode 100644
index 00000000000..3268e240200
--- /dev/null
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+feature 'project owner sees a link to create a license file in empty project', feature: true, js: true do
+ include Select2Helper
+
+ let(:project_master) { create(:user) }
+ let(:project) { create(:empty_project) }
+ background do
+ project.team << [project_master, :master]
+ login_as(project_master)
+ end
+
+ scenario 'project master creates a license file from a template' do
+ visit namespace_project_path(project.namespace, project)
+ click_link 'Create empty bare repository'
+ click_on 'LICENSE'
+
+ expect(current_path).to eq(
+ namespace_project_new_blob_path(project.namespace, project, 'master'))
+ expect(find('#file_name').value).to eq('LICENSE')
+ expect(page).to have_selector('.license-selector')
+
+ select2('mit', from: '#license_type')
+
+ file_content = find('.file-content')
+ expect(file_content).to have_content('The MIT License (MIT)')
+ expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+
+ fill_in :commit_message, with: 'Add a LICENSE file', visible: true
+ # Remove pre-receive hook so we can push without auth
+ FileUtils.rm_f(File.join(project.repository.path, 'hooks', 'pre-receive'))
+ click_button 'Commit Changes'
+
+ expect(current_path).to eq(
+ namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
+ expect(page).to have_content('The MIT License (MIT)')
+ expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ end
+end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index e8886e7edf9..8edeb8d18af 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -80,6 +80,22 @@ describe "Runners" do
end
end
+ describe "shared runners description" do
+ let(:shared_runners_text) { 'custom **shared** runners description' }
+ let(:shared_runners_html) { 'custom shared runners description' }
+
+ before do
+ stub_application_setting(shared_runners_text: shared_runners_text)
+ project = FactoryGirl.create :empty_project, shared_runners_enabled: false
+ project.team << [user, :master]
+ visit runners_path(project)
+ end
+
+ it "sees shared runners description" do
+ expect(page.find(".shared-runners-description")).to have_content(shared_runners_html)
+ end
+ end
+
describe "show page" do
before do
@project = FactoryGirl.create :empty_project
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 79d5bf4cf06..8625ea6bc10 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -101,12 +101,12 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
- it { is_expected.to be_denied_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_denied_for :external }
end
describe "GET /:project_path/blob" do
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index 0a89193eb67..544270b4037 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -101,9 +101,9 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
- it { is_expected.to be_denied_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 40daac89d40..4def4f99bc0 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -101,12 +101,12 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
- it { is_expected.to be_denied_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :visitor }
+ it { is_expected.to be_allowed_for :external }
end
describe "GET /:project_path/builds" do
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
new file mode 100644
index 00000000000..113d4c40cfc
--- /dev/null
+++ b/spec/features/todos/todos_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe 'Dashboard Todos', feature: true do
+ let(:user){ create(:user) }
+ let(:author){ create(:user) }
+ let(:project){ create(:project) }
+ let(:issue){ create(:issue) }
+ let(:todos_per_page){ Todo.default_per_page }
+ let(:todos_total){ todos_per_page + 1 }
+
+ describe 'GET /dashboard/todos' do
+ context 'User does not have todos' do
+ before do
+ login_as(user)
+ visit dashboard_todos_path
+ end
+ it 'shows "All done" message' do
+ expect(page).to have_content "You're all done!"
+ end
+ end
+
+ context 'User has a todo', js: true do
+ before do
+ create(:todo, :mentioned, user: user, project: project, target: issue, author: author)
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it 'todo is present' do
+ expect(page).to have_selector('.todos-list .todo', count: 1)
+ end
+
+ describe 'deleting the todo' do
+ before do
+ first('.done-todo').click
+ end
+
+ it 'is removed from the list' do
+ expect(page).not_to have_selector('.todos-list .todo')
+ end
+
+ it 'shows "All done" message' do
+ expect(page).to have_content("You're all done!")
+ end
+ end
+ end
+
+ context 'User has multiple pages of Todos' do
+ let(:todo_total_pages){ (todos_total.to_f/todos_per_page).ceil }
+
+ before do
+ todos_total.times do
+ create(:todo, :mentioned, user: user, project: project, target: issue, author: author)
+ end
+
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it 'is paginated' do
+ expect(page).to have_selector('.gl-pagination')
+ end
+
+ it 'is has the right number of pages' do
+ expect(page).to have_selector('.gl-pagination .page', count: todo_total_pages)
+ end
+
+ describe 'deleting last todo from last page', js: true do
+ it 'redirects to the previous page' do
+ page.within('.gl-pagination') do
+ click_link todo_total_pages.to_s
+ end
+ first('.done-todo').click
+ expect(page).to have_content(Todo.last.body)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index b1648055462..bc607a29751 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -62,6 +62,22 @@ describe IssuesFinder do
expect(issues).to eq([issue2])
end
+ it 'returns unique issues when filtering by multiple labels' do
+ label2 = create(:label, project: project2)
+
+ create(:label_link, label: label2, target: issue2)
+
+ params = {
+ scope: 'all',
+ label_name: [label.title, label2.title].join(','),
+ state: 'opened'
+ }
+
+ issues = IssuesFinder.new(user, params).execute
+
+ expect(issues).to eq([issue2])
+ end
+
it 'should filter by no label name' do
params = { scope: "all", label_name: Label::None.title, state: 'opened' }
issues = IssuesFinder.new(user, params).execute
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index 4f8d9c67262..f942695b6f0 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -6,8 +6,8 @@ describe CiStatusHelper do
let(:success_commit) { double("Ci::Commit", status: 'success') }
let(:failed_commit) { double("Ci::Commit", status: 'failed') }
- describe 'ci_status_icon' do
- it { expect(helper.ci_status_icon(success_commit)).to include('fa-check') }
- it { expect(helper.ci_status_icon(failed_commit)).to include('fa-close') }
+ describe 'ci_icon_for_status' do
+ it { expect(helper.ci_icon_for_status(success_commit.status)).to include('fa-check') }
+ it { expect(helper.ci_icon_for_status(failed_commit.status)).to include('fa-close') }
end
end
diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb
new file mode 100644
index 00000000000..727c25ff529
--- /dev/null
+++ b/spec/helpers/commits_helper_spec.rb
@@ -0,0 +1,29 @@
+require 'rails_helper'
+
+describe CommitsHelper do
+ describe 'commit_author_link' do
+ it 'escapes the author email' do
+ commit = double(
+ author: nil,
+ author_name: 'Persistent XSS',
+ author_email: 'my@email.com" onmouseover="alert(1)'
+ )
+
+ expect(helper.commit_author_link(commit)).
+ not_to include('onmouseover="alert(1)"')
+ end
+ end
+
+ describe 'commit_committer_link' do
+ it 'escapes the committer email' do
+ commit = double(
+ committer: nil,
+ committer_name: 'Persistent XSS',
+ committer_email: 'my@email.com" onmouseover="alert(1)'
+ )
+
+ expect(helper.commit_committer_link(commit)).
+ not_to include('onmouseover="alert(1)"')
+ end
+ end
+end
diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb
new file mode 100644
index 00000000000..3391234e9f5
--- /dev/null
+++ b/spec/helpers/import_helper_spec.rb
@@ -0,0 +1,25 @@
+require 'rails_helper'
+
+describe ImportHelper do
+ describe '#github_project_link' do
+ context 'when provider does not specify a custom URL' do
+ it 'uses default GitHub URL' do
+ allow(Gitlab.config.omniauth).to receive(:providers).
+ and_return([Settingslogic.new('name' => 'github')])
+
+ expect(helper.github_project_link('octocat/Hello-World')).
+ to include('href="https://github.com/octocat/Hello-World"')
+ end
+ end
+
+ context 'when provider specify a custom URL' do
+ it 'uses custom URL' do
+ allow(Gitlab.config.omniauth).to receive(:providers).
+ and_return([Settingslogic.new('name' => 'github', 'url' => 'https://github.company.com')])
+
+ expect(helper.github_project_link('octocat/Hello-World')).
+ to include('href="https://github.company.com/octocat/Hello-World"')
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 94468abcbb3..b0a38e7c251 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -178,27 +178,37 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
describe 'cross project label references' do
- let(:another_project) { create(:empty_project, :public) }
- let(:project_name) { another_project.name_with_namespace }
- let(:label) { create(:label, project: another_project, color: '#00ff00') }
- let(:reference) { label.to_reference(project) }
+ context 'valid project referenced' do
+ let(:another_project) { create(:empty_project, :public) }
+ let(:project_name) { another_project.name_with_namespace }
+ let(:label) { create(:label, project: another_project, color: '#00ff00') }
+ let(:reference) { label.to_reference(project) }
- let!(:result) { reference_filter("See #{reference}") }
+ let!(:result) { reference_filter("See #{reference}") }
- it 'points to referenced project issues page' do
- expect(result.css('a').first.attr('href'))
- .to eq urls.namespace_project_issues_url(another_project.namespace,
- another_project,
- label_name: label.name)
- end
+ it 'points to referenced project issues page' do
+ expect(result.css('a').first.attr('href'))
+ .to eq urls.namespace_project_issues_url(another_project.namespace,
+ another_project,
+ label_name: label.name)
+ end
+
+ it 'has valid color' do
+ expect(result.css('a span').first.attr('style'))
+ .to match /background-color: #00ff00/
+ end
- it 'has valid color' do
- expect(result.css('a span').first.attr('style'))
- .to match /background-color: #00ff00/
+ it 'contains cross project content' do
+ expect(result.css('a').first.text).to eq "#{label.name} in #{project_name}"
+ end
end
- it 'contains cross project content' do
- expect(result.css('a').first.text).to eq "#{label.name} in #{project_name}"
+ context 'project that does not exist referenced' do
+ let(:result) { reference_filter('aaa/bbb~ccc') }
+
+ it 'does not link reference' do
+ expect(result.to_html).to eq 'aaa/bbb~ccc'
+ end
end
end
end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 5f4b63bcafb..643acf0303c 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -286,6 +286,81 @@ module Ci
end
end
+
+ describe "Scripts handling" do
+ let(:config_data) { YAML.dump(config) }
+ let(:config_processor) { GitlabCiYamlProcessor.new(config_data, path) }
+
+ subject { config_processor.builds_for_stage_and_ref("test", "master").first }
+
+ describe "before_script" do
+ context "in global context" do
+ let(:config) do
+ {
+ before_script: ["global script"],
+ test: { script: ["script"] }
+ }
+ end
+
+ it "return commands with scripts concencaced" do
+ expect(subject[:commands]).to eq("global script\nscript")
+ end
+ end
+
+ context "overwritten in local context" do
+ let(:config) do
+ {
+ before_script: ["global script"],
+ test: { before_script: ["local script"], script: ["script"] }
+ }
+ end
+
+ it "return commands with scripts concencaced" do
+ expect(subject[:commands]).to eq("local script\nscript")
+ end
+ end
+ end
+
+ describe "script" do
+ let(:config) do
+ {
+ test: { script: ["script"] }
+ }
+ end
+
+ it "return commands with scripts concencaced" do
+ expect(subject[:commands]).to eq("script")
+ end
+ end
+
+ describe "after_script" do
+ context "in global context" do
+ let(:config) do
+ {
+ after_script: ["after_script"],
+ test: { script: ["script"] }
+ }
+ end
+
+ it "return after_script in options" do
+ expect(subject[:options][:after_script]).to eq(["after_script"])
+ end
+ end
+
+ context "overwritten in local context" do
+ let(:config) do
+ {
+ after_script: ["local after_script"],
+ test: { after_script: ["local after_script"], script: ["script"] }
+ }
+ end
+
+ it "return after_script in options" do
+ expect(subject[:options][:after_script]).to eq(["local after_script"])
+ end
+ end
+ end
+ end
describe "Image and service handling" do
it "returns image and service when defined" do
@@ -592,7 +667,7 @@ module Ci
stage_idx: 1,
name: :normal_job,
only: nil,
- commands: "\ntest",
+ commands: "test",
tag_list: [],
options: {},
when: "on_success",
@@ -619,7 +694,7 @@ EOT
stage_idx: 1,
name: :job1,
only: nil,
- commands: "\nexecute-script-for-job",
+ commands: "execute-script-for-job",
tag_list: [],
options: {},
when: "on_success",
@@ -631,7 +706,7 @@ EOT
stage_idx: 1,
name: :job2,
only: nil,
- commands: "\nexecute-script-for-job",
+ commands: "execute-script-for-job",
tag_list: [],
options: {},
when: "on_success",
@@ -663,6 +738,27 @@ EOT
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script should be an array of strings")
end
+ it "returns errors if job before_script parameter is not an array of strings" do
+ config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } })
+ expect do
+ GitlabCiYamlProcessor.new(config, path)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: before_script should be an array of strings")
+ end
+
+ it "returns errors if after_script parameter is invalid" do
+ config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config, path)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "after_script should be an array of strings")
+ end
+
+ it "returns errors if job after_script parameter is not an array of strings" do
+ config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } })
+ expect do
+ GitlabCiYamlProcessor.new(config, path)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: after_script should be an array of strings")
+ end
+
it "returns errors if image parameter is invalid" do
config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
expect do
diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb
index 329792bb685..b6f7a2e7ec4 100644
--- a/spec/lib/gitlab/badge/build_spec.rb
+++ b/spec/lib/gitlab/badge/build_spec.rb
@@ -42,7 +42,7 @@ describe Gitlab::Badge::Build do
end
context 'build exists' do
- let(:ci_commit) { create(:ci_commit, project: project, sha: sha) }
+ let(:ci_commit) { create(:ci_commit, project: project, sha: sha, ref: branch) }
let!(:build) { create(:ci_build, commit: ci_commit) }
@@ -57,7 +57,7 @@ describe Gitlab::Badge::Build do
describe '#data' do
let(:data) { badge.data }
- it 'contains infromation about success' do
+ it 'contains information about success' do
expect(status_node(data, 'success')).to be_truthy
end
end
@@ -74,7 +74,7 @@ describe Gitlab::Badge::Build do
describe '#data' do
let(:data) { badge.data }
- it 'contains infromation about failure' do
+ it 'contains information about failure' do
expect(status_node(data, 'failed')).to be_truthy
end
end
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
index 4f3d7f4405b..0e7ffbe9b8e 100644
--- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
@@ -2,13 +2,14 @@ require 'spec_helper'
describe Gitlab::GithubImport::IssueFormatter, lib: true do
let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
- let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') }
+ let(:octocat) { double(id: 123456, login: 'octocat') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
let(:base_data) do
{
number: 1347,
+ milestone: nil,
state: 'open',
title: 'Found a bug',
body: "I'm having a problem with this.",
@@ -26,12 +27,13 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
describe '#attributes' do
context 'when issue is open' do
- let(:raw_data) { OpenStruct.new(base_data.merge(state: 'open')) }
+ let(:raw_data) { double(base_data.merge(state: 'open')) }
it 'returns formatted attributes' do
expected = {
iid: 1347,
project: project,
+ milestone: nil,
title: 'Found a bug',
description: "*Created by: octocat*\n\nI'm having a problem with this.",
state: 'opened',
@@ -47,12 +49,13 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
context 'when issue is closed' do
let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') }
- let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', closed_at: closed_at)) }
+ let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) }
it 'returns formatted attributes' do
expected = {
iid: 1347,
project: project,
+ milestone: nil,
title: 'Found a bug',
description: "*Created by: octocat*\n\nI'm having a problem with this.",
state: 'closed',
@@ -67,7 +70,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end
context 'when it is assigned to someone' do
- let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) }
+ let(:raw_data) { double(base_data.merge(assignee: octocat)) }
it 'returns nil as assignee_id when is not a GitLab user' do
expect(issue.attributes.fetch(:assignee_id)).to be_nil
@@ -80,8 +83,23 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end
end
+ context 'when it has a milestone' do
+ let(:milestone) { double(number: 45) }
+ let(:raw_data) { double(base_data.merge(milestone: milestone)) }
+
+ it 'returns nil when milestone does not exist' do
+ expect(issue.attributes.fetch(:milestone)).to be_nil
+ end
+
+ it 'returns milestone when it exists' do
+ milestone = create(:milestone, project: project, iid: 45)
+
+ expect(issue.attributes.fetch(:milestone)).to eq milestone
+ end
+ end
+
context 'when author is a GitLab user' do
- let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) }
+ let(:raw_data) { double(base_data.merge(user: octocat)) }
it 'returns project#creator_id as author_id when is not a GitLab user' do
expect(issue.attributes.fetch(:author_id)).to eq project.creator_id
@@ -97,7 +115,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
describe '#has_comments?' do
context 'when number of comments is greater than zero' do
- let(:raw_data) { OpenStruct.new(base_data.merge(comments: 1)) }
+ let(:raw_data) { double(base_data.merge(comments: 1)) }
it 'returns true' do
expect(issue.has_comments?).to eq true
@@ -105,7 +123,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end
context 'when number of comments is equal to zero' do
- let(:raw_data) { OpenStruct.new(base_data.merge(comments: 0)) }
+ let(:raw_data) { double(base_data.merge(comments: 0)) }
it 'returns false' do
expect(issue.has_comments?).to eq false
@@ -114,7 +132,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end
describe '#number' do
- let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) }
+ let(:raw_data) { double(base_data.merge(number: 1347)) }
it 'returns pull request number' do
expect(issue.number).to eq 1347
@@ -123,7 +141,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
describe '#valid?' do
context 'when mention a pull request' do
- let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: OpenStruct.new)) }
+ let(:raw_data) { double(base_data.merge(pull_request: double)) }
it 'returns false' do
expect(issue.valid?).to eq false
@@ -131,7 +149,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end
context 'when does not mention a pull request' do
- let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: nil)) }
+ let(:raw_data) { double(base_data.merge(pull_request: nil)) }
it 'returns true' do
expect(issue.valid?).to eq true
diff --git a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb
new file mode 100644
index 00000000000..5a421e50581
--- /dev/null
+++ b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
+ let(:project) { create(:empty_project) }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+ let(:base_data) do
+ {
+ number: 1347,
+ state: 'open',
+ title: '1.0',
+ description: 'Version 1.0',
+ due_on: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil
+ }
+ end
+
+ subject(:formatter) { described_class.new(project, raw_data)}
+
+ describe '#attributes' do
+ context 'when milestone is open' do
+ let(:raw_data) { double(base_data.merge(state: 'open')) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ iid: 1347,
+ project: project,
+ title: '1.0',
+ description: 'Version 1.0',
+ state: 'active',
+ due_date: nil,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(formatter.attributes).to eq(expected)
+ end
+ end
+
+ context 'when milestone is closed' do
+ let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') }
+ let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ iid: 1347,
+ project: project,
+ title: '1.0',
+ description: 'Version 1.0',
+ state: 'closed',
+ due_date: nil,
+ created_at: created_at,
+ updated_at: closed_at
+ }
+
+ expect(formatter.attributes).to eq(expected)
+ end
+ end
+
+ context 'when milestone has a due date' do
+ let(:due_date) { DateTime.strptime('2011-01-28T19:01:12Z') }
+ let(:raw_data) { double(base_data.merge(due_on: due_date)) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ iid: 1347,
+ project: project,
+ title: '1.0',
+ description: 'Version 1.0',
+ state: 'active',
+ due_date: due_date,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(formatter.attributes).to eq(expected)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
index 11249e57ca8..e59c0ca110e 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -2,17 +2,18 @@ require 'spec_helper'
describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:project) { create(:project) }
- let(:repository) { OpenStruct.new(id: 1, fork: false) }
+ let(:repository) { double(id: 1, fork: false) }
let(:source_repo) { repository }
- let(:source_branch) { OpenStruct.new(ref: 'feature', repo: source_repo) }
+ let(:source_branch) { double(ref: 'feature', repo: source_repo) }
let(:target_repo) { repository }
- let(:target_branch) { OpenStruct.new(ref: 'master', repo: target_repo) }
- let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') }
+ let(:target_branch) { double(ref: 'master', repo: target_repo) }
+ let(:octocat) { double(id: 123456, login: 'octocat') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
let(:base_data) do
{
number: 1347,
+ milestone: nil,
state: 'open',
title: 'New feature',
body: 'Please pull these awesome changes',
@@ -31,7 +32,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
describe '#attributes' do
context 'when pull request is open' do
- let(:raw_data) { OpenStruct.new(base_data.merge(state: 'open')) }
+ let(:raw_data) { double(base_data.merge(state: 'open')) }
it 'returns formatted attributes' do
expected = {
@@ -43,6 +44,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
target_project: project,
target_branch: 'master',
state: 'opened',
+ milestone: nil,
author_id: project.creator_id,
assignee_id: nil,
created_at: created_at,
@@ -55,7 +57,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
context 'when pull request is closed' do
let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') }
- let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', closed_at: closed_at)) }
+ let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) }
it 'returns formatted attributes' do
expected = {
@@ -67,6 +69,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
target_project: project,
target_branch: 'master',
state: 'closed',
+ milestone: nil,
author_id: project.creator_id,
assignee_id: nil,
created_at: created_at,
@@ -79,7 +82,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
context 'when pull request is merged' do
let(:merged_at) { DateTime.strptime('2011-01-28T13:01:12Z') }
- let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', merged_at: merged_at)) }
+ let(:raw_data) { double(base_data.merge(state: 'closed', merged_at: merged_at)) }
it 'returns formatted attributes' do
expected = {
@@ -91,6 +94,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
target_project: project,
target_branch: 'master',
state: 'merged',
+ milestone: nil,
author_id: project.creator_id,
assignee_id: nil,
created_at: created_at,
@@ -102,7 +106,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
context 'when it is assigned to someone' do
- let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) }
+ let(:raw_data) { double(base_data.merge(assignee: octocat)) }
it 'returns nil as assignee_id when is not a GitLab user' do
expect(pull_request.attributes.fetch(:assignee_id)).to be_nil
@@ -116,7 +120,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
context 'when author is a GitLab user' do
- let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) }
+ let(:raw_data) { double(base_data.merge(user: octocat)) }
it 'returns project#creator_id as author_id when is not a GitLab user' do
expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id
@@ -128,10 +132,25 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
end
end
+
+ context 'when it has a milestone' do
+ let(:milestone) { double(number: 45) }
+ let(:raw_data) { double(base_data.merge(milestone: milestone)) }
+
+ it 'returns nil when milestone does not exists' do
+ expect(pull_request.attributes.fetch(:milestone)).to be_nil
+ end
+
+ it 'returns milestone when is exists' do
+ milestone = create(:milestone, project: project, iid: 45)
+
+ expect(pull_request.attributes.fetch(:milestone)).to eq milestone
+ end
+ end
end
describe '#number' do
- let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) }
+ let(:raw_data) { double(base_data.merge(number: 1347)) }
it 'returns pull request number' do
expect(pull_request.number).to eq 1347
@@ -139,11 +158,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
describe '#valid?' do
- let(:invalid_branch) { OpenStruct.new(ref: 'invalid-branch') }
+ let(:invalid_branch) { double(ref: 'invalid-branch').as_null_object }
context 'when source, and target repositories are the same' do
context 'and source and target branches exists' do
- let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: target_branch)) }
+ let(:raw_data) { double(base_data.merge(head: source_branch, base: target_branch)) }
it 'returns true' do
expect(pull_request.valid?).to eq true
@@ -151,7 +170,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
context 'and source branch doesn not exists' do
- let(:raw_data) { OpenStruct.new(base_data.merge(head: invalid_branch, base: target_branch)) }
+ let(:raw_data) { double(base_data.merge(head: invalid_branch, base: target_branch)) }
it 'returns false' do
expect(pull_request.valid?).to eq false
@@ -159,7 +178,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
context 'and target branch doesn not exists' do
- let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: invalid_branch)) }
+ let(:raw_data) { double(base_data.merge(head: source_branch, base: invalid_branch)) }
it 'returns false' do
expect(pull_request.valid?).to eq false
@@ -168,8 +187,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
context 'when source repo is a fork' do
- let(:source_repo) { OpenStruct.new(id: 2, fork: true) }
- let(:raw_data) { OpenStruct.new(base_data) }
+ let(:source_repo) { double(id: 2, fork: true) }
+ let(:raw_data) { double(base_data) }
it 'returns false' do
expect(pull_request.valid?).to eq false
@@ -177,8 +196,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
context 'when target repo is a fork' do
- let(:target_repo) { OpenStruct.new(id: 2, fork: true) }
- let(:raw_data) { OpenStruct.new(base_data) }
+ let(:target_repo) { double(id: 2, fork: true) }
+ let(:raw_data) { double(base_data) }
it 'returns false' do
expect(pull_request.valid?).to eq false
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 10177c0e8dd..96f7eabbca6 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -123,4 +123,28 @@ describe Gitlab::Metrics do
end
end
end
+
+ describe '.action=' do
+ context 'without a transaction' do
+ it 'does nothing' do
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ not_to receive(:action=)
+
+ Gitlab::Metrics.action = 'foo'
+ end
+ end
+
+ context 'with a transaction' do
+ it 'sets the action of a transaction' do
+ trans = Gitlab::Metrics::Transaction.new
+
+ expect(Gitlab::Metrics).to receive(:current_transaction).
+ and_return(trans)
+
+ expect(trans).to receive(:action=).with('foo')
+
+ Gitlab::Metrics.action = 'foo'
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb
index 961022b9d12..7fc34139eff 100644
--- a/spec/lib/gitlab/push_data_builder_spec.rb
+++ b/spec/lib/gitlab/push_data_builder_spec.rb
@@ -14,11 +14,11 @@ describe Gitlab::PushDataBuilder, lib: true do
it { expect(data[:ref]).to eq('refs/heads/master') }
it { expect(data[:commits].size).to eq(3) }
it { expect(data[:total_commits_count]).to eq(3) }
- it { expect(data[:commits].first[:added]).to eq(["gitlab-grack"]) }
- it { expect(data[:commits].first[:modified]).to eq([".gitmodules"]) }
+ it { expect(data[:commits].first[:added]).to eq(['gitlab-grack']) }
+ it { expect(data[:commits].first[:modified]).to eq(['.gitmodules']) }
it { expect(data[:commits].first[:removed]).to eq([]) }
- include_examples 'project hook data'
+ include_examples 'project hook data with deprecateds'
include_examples 'deprecated repository hook data'
end
@@ -34,9 +34,18 @@ describe Gitlab::PushDataBuilder, lib: true do
it { expect(data[:checkout_sha]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
it { expect(data[:after]).to eq('8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b') }
it { expect(data[:ref]).to eq('refs/tags/v1.1.0') }
+ it { expect(data[:user_id]).to eq(user.id) }
+ it { expect(data[:user_name]).to eq(user.name) }
+ it { expect(data[:user_email]).to eq(user.email) }
+ it { expect(data[:user_avatar]).to eq(user.avatar_url) }
+ it { expect(data[:project_id]).to eq(project.id) }
+ it { expect(data[:project]).to be_a(Hash) }
it { expect(data[:commits]).to be_empty }
it { expect(data[:total_commits_count]).to be_zero }
+ include_examples 'project hook data with deprecateds'
+ include_examples 'deprecated repository hook data'
+
it 'does not raise an error when given nil commits' do
expect { described_class.build(spy, spy, spy, spy, spy, nil) }.
not_to raise_error
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index 6ffc0d6e658..bf11472407a 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -106,5 +106,14 @@ describe Gitlab::UrlBuilder, lib: true do
end
end
end
+
+ context 'when passing a WikiPage' do
+ it 'returns a proper URL' do
+ wiki_page = build(:wiki_page)
+ url = described_class.build(wiki_page)
+
+ expect(url).to eq "#{Gitlab.config.gitlab.url}#{wiki_page.wiki.wiki_base_path}/#{wiki_page.slug}"
+ end
+ end
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 631b5094f42..495c5cbac00 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -213,7 +213,7 @@ describe Notify do
it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
+ is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
end
it 'contains a link to the new merge request' do
@@ -268,7 +268,7 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
+ is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
end
it 'contains the name of the previous assignee' do
@@ -302,7 +302,7 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
+ is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
end
it 'contains the names of the added labels' do
@@ -331,7 +331,7 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/i
+ is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/i
end
it 'contains the new status' do
@@ -364,7 +364,7 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
+ is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
end
it 'contains the new status' do
@@ -502,7 +502,7 @@ describe Notify do
it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
+ is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
end
it 'contains a link to the merge request note' do
diff --git a/spec/mailers/repository_check_mailer_spec.rb b/spec/mailers/repository_check_mailer_spec.rb
index 583bf15176f..00613c7b671 100644
--- a/spec/mailers/repository_check_mailer_spec.rb
+++ b/spec/mailers/repository_check_mailer_spec.rb
@@ -15,7 +15,7 @@ describe RepositoryCheckMailer do
it 'mentions the number of failed checks' do
mail = described_class.notify(3)
- expect(mail).to have_subject '3 projects failed their last repository check'
+ expect(mail).to have_subject 'GitLab Admin | 3 projects failed their last repository check'
end
end
end
diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb
index 412842337ba..82c18aaa01a 100644
--- a/spec/models/ci/commit_spec.rb
+++ b/spec/models/ci/commit_spec.rb
@@ -27,6 +27,8 @@ describe Ci::Commit, models: true do
it { is_expected.to have_many(:trigger_requests) }
it { is_expected.to have_many(:builds) }
it { is_expected.to validate_presence_of :sha }
+ it { is_expected.to validate_presence_of :status }
+ it { is_expected.to delegate_method(:stages).to(:statuses) }
it { is_expected.to respond_to :git_author_name }
it { is_expected.to respond_to :git_author_email }
@@ -52,57 +54,9 @@ describe Ci::Commit, models: true do
it { expect(commit.sha).to start_with(subject) }
end
- describe :stage do
- subject { commit.stage }
-
- before do
- @second = FactoryGirl.create :commit_status, commit: commit, name: 'deploy', stage: 'deploy', stage_idx: 1, status: 'pending'
- @first = FactoryGirl.create :commit_status, commit: commit, name: 'test', stage: 'test', stage_idx: 0, status: 'pending'
- end
-
- it 'returns first running stage' do
- is_expected.to eq('test')
- end
-
- context 'first build succeeded' do
- before do
- @first.success
- end
-
- it 'returns last running stage' do
- is_expected.to eq('deploy')
- end
- end
-
- context 'all builds succeeded' do
- before do
- @first.success
- @second.success
- end
-
- it 'returns nil' do
- is_expected.to be_nil
- end
- end
- end
-
describe :create_next_builds do
end
- describe :refs do
- subject { commit.refs }
-
- before do
- FactoryGirl.create :commit_status, commit: commit, name: 'deploy'
- FactoryGirl.create :commit_status, commit: commit, name: 'deploy', ref: 'develop'
- FactoryGirl.create :commit_status, commit: commit, name: 'deploy', ref: 'master'
- end
-
- it 'returns all refs' do
- is_expected.to contain_exactly('master', 'develop', nil)
- end
- end
-
describe :retried do
subject { commit.retried }
@@ -117,10 +71,10 @@ describe Ci::Commit, models: true do
end
describe :create_builds do
- let!(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let!(:commit) { FactoryGirl.create :ci_commit, project: project, ref: 'master', tag: false }
def create_builds(trigger_request = nil)
- commit.create_builds('master', false, nil, trigger_request)
+ commit.create_builds(nil, trigger_request)
end
def create_next_builds
@@ -143,67 +97,6 @@ describe Ci::Commit, models: true do
expect(create_next_builds).to be_falsey
end
- context 'for different ref' do
- def create_develop_builds
- commit.create_builds('develop', false, nil, nil)
- end
-
- it 'creates builds' do
- expect(create_builds).to be_truthy
- commit.builds.update_all(status: "success")
- expect(commit.builds.count(:all)).to eq(2)
-
- expect(create_develop_builds).to be_truthy
- commit.builds.update_all(status: "success")
- expect(commit.builds.count(:all)).to eq(4)
- expect(commit.refs.size).to eq(2)
- expect(commit.builds.pluck(:name).uniq.size).to eq(2)
- end
- end
-
- context 'for build triggers' do
- let(:trigger) { FactoryGirl.create :ci_trigger, project: project }
- let(:trigger_request) { FactoryGirl.create :ci_trigger_request, commit: commit, trigger: trigger }
-
- it 'creates builds' do
- expect(create_builds(trigger_request)).to be_truthy
- expect(commit.builds.count(:all)).to eq(2)
- end
-
- it 'rebuilds commit' do
- expect(create_builds).to be_truthy
- expect(commit.builds.count(:all)).to eq(2)
-
- expect(create_builds(trigger_request)).to be_truthy
- expect(commit.builds.count(:all)).to eq(4)
- end
-
- it 'creates next builds' do
- expect(create_builds(trigger_request)).to be_truthy
- expect(commit.builds.count(:all)).to eq(2)
- commit.builds.update_all(status: "success")
-
- expect(create_next_builds).to be_truthy
- expect(commit.builds.count(:all)).to eq(4)
- end
-
- context 'for [ci skip]' do
- before do
- allow(commit).to receive(:git_commit_message) { 'message [ci skip]' }
- end
-
- it 'rebuilds commit' do
- expect(commit.status).to eq('skipped')
- expect(create_builds).to be_truthy
-
- # since everything in Ci::Commit is cached we need to fetch a new object
- new_commit = Ci::Commit.find_by_id(commit.id)
- expect(new_commit.status).to eq('pending')
- end
- end
- end
-
-
context 'custom stage with first job allowed to fail' do
let(:yaml) do
{
@@ -284,6 +177,7 @@ describe Ci::Commit, models: true do
commit.builds.running_or_pending.each(&:success)
expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
+ commit.reload
expect(commit.status).to eq('success')
end
@@ -306,6 +200,7 @@ describe Ci::Commit, models: true do
commit.builds.running_or_pending.each(&:success)
expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
+ commit.reload
expect(commit.status).to eq('failed')
end
@@ -329,6 +224,7 @@ describe Ci::Commit, models: true do
expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success')
+ commit.reload
expect(commit.status).to eq('failed')
end
@@ -351,6 +247,7 @@ describe Ci::Commit, models: true do
commit.builds.running_or_pending.each(&:success)
expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
+ commit.reload
expect(commit.status).to eq('failed')
end
end
@@ -402,4 +299,98 @@ describe Ci::Commit, models: true do
expect(commit.coverage).to be_nil
end
end
+
+ describe '#retryable?' do
+ subject { commit.retryable? }
+
+ context 'no failed builds' do
+ before do
+ FactoryGirl.create :ci_build, name: "rspec", commit: commit, status: 'success'
+ end
+
+ it 'be not retryable' do
+ is_expected.to be_falsey
+ end
+ end
+
+ context 'with failed builds' do
+ before do
+ FactoryGirl.create :ci_build, name: "rspec", commit: commit, status: 'running'
+ FactoryGirl.create :ci_build, name: "rubocop", commit: commit, status: 'failed'
+ end
+
+ it 'be retryable' do
+ is_expected.to be_truthy
+ end
+ end
+ end
+
+ describe '#stages' do
+ let(:commit2) { FactoryGirl.create :ci_commit, project: project }
+ subject { CommitStatus.where(commit: [commit, commit2]).stages }
+
+ before do
+ FactoryGirl.create :ci_build, commit: commit2, stage: 'test', stage_idx: 1
+ FactoryGirl.create :ci_build, commit: commit, stage: 'build', stage_idx: 0
+ end
+
+ it 'return all stages' do
+ is_expected.to eq(%w(build test))
+ end
+ end
+
+ describe '#update_state' do
+ it 'execute update_state after touching object' do
+ expect(commit).to receive(:update_state).and_return(true)
+ commit.touch
+ end
+
+ context 'dependent objects' do
+ let(:commit_status) { build :commit_status, commit: commit }
+
+ it 'execute update_state after saving dependent object' do
+ expect(commit).to receive(:update_state).and_return(true)
+ commit_status.save
+ end
+ end
+
+ context 'update state' do
+ let(:current) { Time.now.change(usec: 0) }
+ let(:build) { FactoryGirl.create :ci_build, :success, commit: commit, started_at: current - 120, finished_at: current - 60 }
+
+ before do
+ build
+ end
+
+ [:status, :started_at, :finished_at, :duration].each do |param|
+ it "update #{param}" do
+ expect(commit.send(param)).to eq(build.send(param))
+ end
+ end
+ end
+ end
+
+ describe '#branch?' do
+ subject { commit.branch? }
+
+ context 'is not a tag' do
+ before do
+ commit.tag = false
+ end
+
+ it 'return true when tag is set to false' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'is not a tag' do
+ before do
+ commit.tag = true
+ end
+
+ it 'return false when tag is set to true' do
+ is_expected.to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 0e9111c8029..ad47e338a33 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -163,4 +163,12 @@ eos
it { expect(commit.reverts_commit?(another_commit)).to be_truthy }
end
end
+
+ describe '#ci_commits' do
+ # TODO: kamil
+ end
+
+ describe '#status' do
+ # TODO: kamil
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 82c68ff6cb1..971e6750375 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -163,37 +163,73 @@ describe CommitStatus, models: true do
end
it 'return unique statuses' do
- is_expected.to eq([@commit2, @commit3, @commit4, @commit5])
+ is_expected.to eq([@commit4, @commit5])
end
end
- describe :for_ref do
- subject { CommitStatus.for_ref('bb').order(:id) }
+ describe :running_or_pending do
+ subject { CommitStatus.running_or_pending.order(:id) }
before do
@commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running'
@commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending'
@commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success'
+ @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'dd', ref: nil, status: 'failed'
+ @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'ee', ref: nil, status: 'canceled'
end
- it 'return statuses with equal and nil ref set' do
- is_expected.to eq([@commit1])
+ it 'return statuses that are running or pending' do
+ is_expected.to eq([@commit1, @commit2])
end
end
- describe :running_or_pending do
- subject { CommitStatus.running_or_pending.order(:id) }
+ describe '#before_sha' do
+ subject { commit_status.before_sha }
+
+ context 'when no before_sha is set for ci::commit' do
+ before { commit.before_sha = nil }
+
+ it 'return blank sha' do
+ is_expected.to eq(Gitlab::Git::BLANK_SHA)
+ end
+ end
+
+ context 'for before_sha set for ci::commit' do
+ let(:value) { '1234' }
+ before { commit.before_sha = value }
+
+ it 'return the set value' do
+ is_expected.to eq(value)
+ end
+ end
+ end
+ describe '#stages' do
before do
- @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running'
- @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending'
- @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success'
- @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'dd', ref: nil, status: 'failed'
- @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'ee', ref: nil, status: 'canceled'
+ FactoryGirl.create :commit_status, commit: commit, stage: 'build', stage_idx: 0, status: 'success'
+ FactoryGirl.create :commit_status, commit: commit, stage: 'build', stage_idx: 0, status: 'failed'
+ FactoryGirl.create :commit_status, commit: commit, stage: 'deploy', stage_idx: 2, status: 'running'
+ FactoryGirl.create :commit_status, commit: commit, stage: 'test', stage_idx: 1, status: 'success'
end
- it 'return statuses that are running or pending' do
- is_expected.to eq([@commit1, @commit2])
+ context 'stages list' do
+ subject { CommitStatus.where(commit: commit).stages }
+
+ it 'return ordered list of stages' do
+ is_expected.to eq(%w(build test deploy))
+ end
+ end
+
+ context 'stages with statuses' do
+ subject { CommitStatus.where(commit: commit).stages_status }
+
+ it 'return list of stages with statuses' do
+ is_expected.to eq({
+ 'build' => 'failed',
+ 'test' => 'success',
+ 'deploy' => 'running'
+ })
+ end
end
end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index b16ccc6e305..4a4cd093435 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -212,4 +212,34 @@ describe Issue, "Issuable" do
expect(issue.downvotes).to eq(1)
end
end
+
+ describe ".with_label" do
+ let(:project) { create(:project, :public) }
+ let(:bug) { create(:label, project: project, title: 'bug') }
+ let(:feature) { create(:label, project: project, title: 'feature') }
+ let(:enhancement) { create(:label, project: project, title: 'enhancement') }
+ let(:issue1) { create(:issue, title: "Bugfix1", project: project) }
+ let(:issue2) { create(:issue, title: "Bugfix2", project: project) }
+ let(:issue3) { create(:issue, title: "Feature1", project: project) }
+
+ before(:each) do
+ issue1.labels << bug
+ issue1.labels << feature
+ issue2.labels << bug
+ issue2.labels << enhancement
+ issue3.labels << feature
+ end
+
+ it 'finds the correct issue containing just enhancement label' do
+ expect(Issue.with_label(enhancement.title)).to match_array([issue2])
+ end
+
+ it 'finds the correct issues containing the same label' do
+ expect(Issue.with_label(bug.title)).to match_array([issue1, issue2])
+ end
+
+ it 'finds the correct issues containing only both labels' do
+ expect(Issue.with_label([bug.title, enhancement.title])).to match_array([issue2])
+ end
+ end
end
diff --git a/spec/lib/ci/status_spec.rb b/spec/models/concerns/statuseable_spec.rb
index 47f3df6e3ce..dacbd3034c0 100644
--- a/spec/lib/ci/status_spec.rb
+++ b/spec/models/concerns/statuseable_spec.rb
@@ -1,8 +1,17 @@
require 'spec_helper'
-describe Ci::Status do
- describe '.get_status' do
- subject { described_class.get_status(statuses) }
+describe Statuseable do
+ before do
+ @object = Object.new
+ @object.extend(Statuseable::ClassMethods)
+ end
+
+ describe '.status' do
+ before do
+ allow(@object).to receive(:all).and_return(CommitStatus.where(id: statuses))
+ end
+
+ subject { @object.status }
shared_examples 'build status summary' do
context 'all successful' do
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index fac516f9568..060e6599104 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -191,18 +191,36 @@ describe Issue, models: true do
end
describe '#related_branches' do
- it 'selects the right branches' do
+ let(:user) { build(:admin) }
+
+ before do
allow(subject.project.repository).to receive(:branch_names).
- and_return(['mpempe', "#{subject.iid}mepmep", subject.to_branch_name])
+ and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name, "#{subject.iid}-branch"])
+
+ # Without this stub, the `create(:merge_request)` above fails because it can't find
+ # the source branch. This seems like a reasonable compromise, in comparison with
+ # setting up a full repo here.
+ allow_any_instance_of(MergeRequest).to receive(:create_merge_request_diff)
+ end
+
+ it "selects the right branches when there are no referenced merge requests" do
+ expect(subject.related_branches(user)).to eq([subject.to_branch_name, "#{subject.iid}-branch"])
+ end
- expect(subject.related_branches).to eq([subject.to_branch_name])
+ it "selects the right branches when there is a referenced merge request" do
+ merge_request = create(:merge_request, { description: "Closes ##{subject.iid}",
+ source_project: subject.project,
+ source_branch: "#{subject.iid}-branch" })
+ merge_request.create_cross_references!(user)
+ expect(subject.referenced_merge_requests).to_not be_empty
+ expect(subject.related_branches(user)).to eq([subject.to_branch_name])
end
it 'excludes stable branches from the related branches' do
allow(subject.project.repository).to receive(:branch_names).
and_return(["#{subject.iid}-0-stable"])
- expect(subject.related_branches).to eq []
+ expect(subject.related_branches(user)).to eq []
end
end
@@ -217,11 +235,20 @@ describe Issue, models: true do
let(:subject) { create :issue }
end
- describe '#to_branch_name' do
- let(:issue) { create(:issue, title: 'a' * 30) }
+ describe "#to_branch_name" do
+ let(:issue) { create(:issue, title: 'testing-issue') }
it 'starts with the issue iid' do
- expect(issue.to_branch_name).to match /\A#{issue.iid}-a+\z/
+ expect(issue.to_branch_name).to match /\A#{issue.iid}-[A-Za-z\-]+\z/
+ end
+
+ it "contains the issue title if not confidential" do
+ expect(issue.to_branch_name).to match /testing-issue\z/
+ end
+
+ it "does not contain the issue title if confidential" do
+ issue = create(:issue, title: 'testing-issue', confidential: true)
+ expect(issue.to_branch_name).to match /confidential-issue\z/
end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 6f5d912fe5d..d7884cea336 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -404,12 +404,12 @@ describe MergeRequest, models: true do
describe 'when the source project exists' do
it 'returns the latest commit' do
commit = double(:commit, id: '123abc')
- ci_commit = double(:ci_commit)
+ ci_commit = double(:ci_commit, ref: 'master')
allow(subject).to receive(:last_commit).and_return(commit)
expect(subject.source_project).to receive(:ci_commit).
- with('123abc').
+ with('123abc', 'master').
and_return(ci_commit)
expect(subject.ci_commit).to eq(ci_commit)
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 91dd92b7c67..d878162a220 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -152,7 +152,7 @@ describe HipchatService, models: true do
obj_attr = merge_sample_data[:object_attributes]
expect(message).to eq("#{user.name} opened " \
- "<a href=\"#{obj_attr[:url]}\">merge request ##{obj_attr["iid"]}</a> in " \
+ "<a href=\"#{obj_attr[:url]}\">merge request !#{obj_attr["iid"]}</a> in " \
"<a href=\"#{project.web_url}\">#{project_name}</a>: " \
"<b>Awesome merge request</b>" \
"<pre>please fix</pre>")
@@ -202,7 +202,7 @@ describe HipchatService, models: true do
title = data[:merge_request]['title']
expect(message).to eq("#{user.name} commented on " \
- "<a href=\"#{obj_attr[:url]}\">merge request ##{merge_id}</a> in " \
+ "<a href=\"#{obj_attr[:url]}\">merge request !#{merge_id}</a> in " \
"<a href=\"#{project.web_url}\">#{project_name}</a>: " \
"<b>#{title}</b>" \
"<pre>merge request note</pre>")
diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb
index dae8bd90922..224c7ceabe8 100644
--- a/spec/models/project_services/slack_service/merge_message_spec.rb
+++ b/spec/models/project_services/slack_service/merge_message_spec.rb
@@ -31,7 +31,7 @@ describe SlackService::MergeMessage, models: true do
context 'open' do
it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq(
- 'Test User opened <somewhere.com/merge_requests/100|merge request #100> '\
+ 'Test User opened <somewhere.com/merge_requests/100|merge request !100> '\
'in <somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty
end
@@ -43,7 +43,7 @@ describe SlackService::MergeMessage, models: true do
end
it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq(
- 'Test User closed <somewhere.com/merge_requests/100|merge request #100> '\
+ 'Test User closed <somewhere.com/merge_requests/100|merge request !100> '\
'in <somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty
end
diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb
index 06006b9a4f5..d37590cab75 100644
--- a/spec/models/project_services/slack_service/note_message_spec.rb
+++ b/spec/models/project_services/slack_service/note_message_spec.rb
@@ -63,7 +63,7 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on a merge request' do
message = SlackService::NoteMessage.new(@args)
expect(message.pretext).to eq("Test User commented on " \
- "<url|merge request #30> in <somewhere.com|project_name>: " \
+ "<url|merge request !30> in <somewhere.com|project_name>: " \
"*merge request title*")
expected_attachments = [
{
diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
new file mode 100644
index 00000000000..6ecab645b49
--- /dev/null
+++ b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe SlackService::WikiPageMessage, models: true do
+ subject { described_class.new(args) }
+
+ let(:args) do
+ {
+ user: {
+ name: 'Test User',
+ username: 'Test User'
+ },
+ project_name: 'project_name',
+ project_url: 'somewhere.com',
+ object_attributes: {
+ title: 'Wiki page title',
+ url: 'url',
+ content: 'Wiki page description'
+ }
+ }
+ end
+
+ describe '#pretext' do
+ context 'when :action == "create"' do
+ before { args[:object_attributes][:action] = 'create' }
+
+ it 'returns a message that a new wiki page was created' do
+ expect(subject.pretext).to eq(
+ 'Test User created <url|wiki page> in <somewhere.com|project_name>: '\
+ '*Wiki page title*')
+ end
+ end
+
+ context 'when :action == "update"' do
+ before { args[:object_attributes][:action] = 'update' }
+
+ it 'returns a message that a wiki page was updated' do
+ expect(subject.pretext).to eq(
+ 'Test User edited <url|wiki page> in <somewhere.com|project_name>: '\
+ '*Wiki page title*')
+ end
+ end
+ end
+
+ describe '#attachments' do
+ let(:color) { '#345' }
+
+ context 'when :action == "create"' do
+ before { args[:object_attributes][:action] = 'create' }
+
+
+ it 'it returns the attachment for a new wiki page' do
+ expect(subject.attachments).to eq([
+ {
+ text: "Wiki page description",
+ color: color,
+ }
+ ])
+ end
+ end
+
+ context 'when :action == "update"' do
+ before { args[:object_attributes][:action] = 'update' }
+
+ it 'it returns the attachment for an updated wiki page' do
+ expect(subject.attachments).to eq([
+ {
+ text: "Wiki page description",
+ color: color,
+ }
+ ])
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index a9e0afad90f..478d59be08b 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -75,6 +75,17 @@ describe SlackService, models: true do
@merge_request = merge_service.execute
@merge_sample_data = merge_service.hook_data(@merge_request,
'open')
+
+ opts = {
+ title: "Awesome wiki_page",
+ content: "Some text describing some thing or another",
+ format: "md",
+ message: "user created page: Awesome wiki_page"
+ }
+
+ wiki_page_service = WikiPages::CreateService.new(project, user, opts)
+ @wiki_page = wiki_page_service.execute
+ @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create')
end
it "should call Slack API for push events" do
@@ -95,6 +106,12 @@ describe SlackService, models: true do
expect(WebMock).to have_requested(:post, webhook_url).once
end
+ it "should call Slack API for wiki page events" do
+ slack.execute(@wiki_page_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
it 'should use the username as an option for slack when configured' do
allow(slack).to receive(:username).and_return(username)
expect(Slack::Notifier).to receive(:new).
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f29c389e094..becc743de31 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -441,9 +441,22 @@ describe Project, models: true do
describe :ci_commit do
let(:project) { create :project }
- let(:commit) { create :ci_commit, project: project }
+ let(:commit) { create :ci_commit, project: project, ref: 'master' }
- it { expect(project.ci_commit(commit.sha)).to eq(commit) }
+ subject { project.ci_commit(commit.sha, 'master') }
+
+ it { is_expected.to eq(commit) }
+
+ context 'return latest' do
+ let(:commit2) { create :ci_commit, project: project, ref: 'master' }
+
+ before do
+ commit
+ commit2
+ end
+
+ it { is_expected.to eq(commit2) }
+ end
end
describe :builds_enabled do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index f30a21e79ae..b561aa663d1 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -135,22 +135,69 @@ describe Repository, models: true do
end
- describe "#license" do
+ describe '#license_blob' do
before do
- repository.send(:cache).expire(:license)
+ repository.send(:cache).expire(:license_blob)
+ repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
end
- it 'test selection preference' do
- files = [TestBlob.new('file'), TestBlob.new('license'), TestBlob.new('copying')]
- expect(repository.tree).to receive(:blobs).and_return(files)
+ it 'looks in the root_ref only' do
+ repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'markdown')
+ repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'markdown', false)
+
+ expect(repository.license_blob).to be_nil
+ end
- expect(repository.license.name).to eq('license')
+ it 'favors license file with no extension' do
+ repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false)
+ repository.commit_file(user, 'LICENSE.md', Licensee::License.new('mit').content, 'Add LICENSE.md', 'master', false)
+
+ expect(repository.license_blob.name).to eq('LICENSE')
end
- it 'also accepts licence instead of license' do
- expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('licence')])
+ it 'favors .md file to .txt' do
+ repository.commit_file(user, 'LICENSE.md', Licensee::License.new('mit').content, 'Add LICENSE.md', 'master', false)
+ repository.commit_file(user, 'LICENSE.txt', Licensee::License.new('mit').content, 'Add LICENSE.txt', 'master', false)
- expect(repository.license.name).to eq('licence')
+ expect(repository.license_blob.name).to eq('LICENSE.md')
+ end
+
+ it 'favors LICENCE to LICENSE' do
+ repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false)
+ repository.commit_file(user, 'LICENCE', Licensee::License.new('mit').content, 'Add LICENCE', 'master', false)
+
+ expect(repository.license_blob.name).to eq('LICENCE')
+ end
+
+ it 'favors LICENSE to COPYING' do
+ repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false)
+ repository.commit_file(user, 'COPYING', Licensee::License.new('mit').content, 'Add COPYING', 'master', false)
+
+ expect(repository.license_blob.name).to eq('LICENSE')
+ end
+
+ it 'favors LICENCE to COPYING' do
+ repository.commit_file(user, 'LICENCE', Licensee::License.new('mit').content, 'Add LICENCE', 'master', false)
+ repository.commit_file(user, 'COPYING', Licensee::License.new('mit').content, 'Add COPYING', 'master', false)
+
+ expect(repository.license_blob.name).to eq('LICENCE')
+ end
+ end
+
+ describe '#license_key' do
+ before do
+ repository.send(:cache).expire(:license_key)
+ repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
+ end
+
+ it 'returns "no-license" when no license is detected' do
+ expect(repository.license_key).to eq('no-license')
+ end
+
+ it 'returns the license key' do
+ repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false)
+
+ expect(repository.license_key).to eq('mit')
end
end
@@ -541,6 +588,41 @@ describe Repository, models: true do
end
end
+ describe '#cherry_pick' do
+ let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') }
+ let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
+ let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
+
+ context 'when there is a conflict' do
+ it 'should abort the operation' do
+ expect(repository.cherry_pick(user, conflict_commit, 'master')).to eq(false)
+ end
+ end
+
+ context 'when commit was already cherry-picked' do
+ it 'should abort the operation' do
+ repository.cherry_pick(user, pickable_commit, 'master')
+
+ expect(repository.cherry_pick(user, pickable_commit, 'master')).to eq(false)
+ end
+ end
+
+ context 'when commit can be cherry-picked' do
+ it 'should cherry-pick the changes' do
+ expect(repository.cherry_pick(user, pickable_commit, 'master')).to be_truthy
+ end
+ end
+
+ context 'cherry-picking a merge commit' do
+ it 'should cherry-pick the changes' do
+ expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).to be_nil
+
+ repository.cherry_pick(user, pickable_merge, 'master')
+ expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).not_to be_nil
+ end
+ end
+ end
+
describe '#before_delete' do
describe 'when a repository does not exist' do
before do
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 967c34800d0..5ead735be48 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -59,7 +59,7 @@ describe API::API, api: true do
describe 'GET /projects/:id/repository/commits/:sha/builds' do
before do
- project.ensure_ci_commit(commit.sha)
+ project.ensure_ci_commit(commit.sha, 'master')
get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", api_user)
end
diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb
index 429a24109fd..f3785b19362 100644
--- a/spec/requests/api/commit_status_spec.rb
+++ b/spec/requests/api/commit_status_spec.rb
@@ -16,7 +16,8 @@ describe API::CommitStatus, api: true do
let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" }
context 'ci commit exists' do
- let!(:ci_commit) { project.ensure_ci_commit(commit.id) }
+ let!(:master) { project.ci_commits.create(sha: commit.id, ref: 'master') }
+ let!(:develop) { project.ci_commits.create(sha: commit.id, ref: 'develop') }
it_behaves_like 'a paginated resources' do
let(:request) { get api(get_url, reporter) }
@@ -25,16 +26,16 @@ describe API::CommitStatus, api: true do
context "reporter user" do
let(:statuses_id) { json_response.map { |status| status['id'] } }
- def create_status(opts = {})
- create(:commit_status, { commit: ci_commit }.merge(opts))
+ def create_status(commit, opts = {})
+ create(:commit_status, { commit: commit, ref: commit.ref }.merge(opts))
end
- let!(:status1) { create_status(status: 'running') }
- let!(:status2) { create_status(name: 'coverage', status: 'pending') }
- let!(:status3) { create_status(ref: 'develop', status: 'running', allow_failure: true) }
- let!(:status4) { create_status(name: 'coverage', status: 'success') }
- let!(:status5) { create_status(name: 'coverage', ref: 'develop', status: 'success') }
- let!(:status6) { create_status(status: 'success') }
+ let!(:status1) { create_status(master, status: 'running') }
+ let!(:status2) { create_status(master, name: 'coverage', status: 'pending') }
+ let!(:status3) { create_status(develop, status: 'running', allow_failure: true) }
+ let!(:status4) { create_status(master, name: 'coverage', status: 'success') }
+ let!(:status5) { create_status(develop, name: 'coverage', status: 'success') }
+ let!(:status6) { create_status(master, status: 'success') }
context 'latest commit statuses' do
before { get api(get_url, reporter) }
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 7ff21175c1b..e28998d51b5 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -48,14 +48,14 @@ describe API::API, api: true do
expect(response.status).to eq(404)
end
- it "should return not_found for CI status" do
+ it "should return nil for commit without CI" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
expect(response.status).to eq(200)
- expect(json_response['status']).to eq('not_found')
+ expect(json_response['status']).to be_nil
end
it "should return status for CI" do
- ci_commit = project.ensure_ci_commit(project.repository.commit.sha)
+ ci_commit = project.ensure_ci_commit(project.repository.commit.sha, 'master')
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
expect(response.status).to eq(200)
expect(json_response['status']).to eq(ci_commit.status)
diff --git a/spec/requests/api/licenses_spec.rb b/spec/requests/api/licenses_spec.rb
new file mode 100644
index 00000000000..c17dcb222a9
--- /dev/null
+++ b/spec/requests/api/licenses_spec.rb
@@ -0,0 +1,136 @@
+require 'spec_helper'
+
+describe API::Licenses, api: true do
+ include ApiHelpers
+
+ describe 'Entity' do
+ before { get api('/licenses/mit') }
+
+ it { expect(json_response['key']).to eq('mit') }
+ it { expect(json_response['name']).to eq('MIT License') }
+ it { expect(json_response['nickname']).to be_nil }
+ it { expect(json_response['popular']).to be true }
+ it { expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/') }
+ it { expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT') }
+ it { expect(json_response['description']).to include('A permissive license that is short and to the point.') }
+ it { expect(json_response['conditions']).to eq(%w[include-copyright]) }
+ it { expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use]) }
+ it { expect(json_response['limitations']).to eq(%w[no-liability]) }
+ it { expect(json_response['content']).to include('The MIT License (MIT)') }
+ end
+
+ describe 'GET /licenses' do
+ it 'returns a list of available license templates' do
+ get api('/licenses')
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(15)
+ expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
+ end
+
+ describe 'the popular parameter' do
+ context 'with popular=1' do
+ it 'returns a list of available popular license templates' do
+ get api('/licenses?popular=1')
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(3)
+ expect(json_response.map { |l| l['key'] }).to include('apache-2.0')
+ end
+ end
+ end
+ end
+
+ describe 'GET /licenses/:key' do
+ context 'with :project and :fullname given' do
+ before do
+ get api("/licenses/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}")
+ end
+
+ context 'for the mit license' do
+ let(:license_type) { 'mit' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('The MIT License (MIT)')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('Copyright (c) 2016 Anton')
+ end
+ end
+
+ context 'for the agpl-3.0 license' do
+ let(:license_type) { 'agpl-3.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('GNU AFFERO GENERAL PUBLIC LICENSE')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('My Awesome Project')
+ expect(json_response['content']).to include('Copyright (C) 2016 Anton')
+ end
+ end
+
+ context 'for the gpl-3.0 license' do
+ let(:license_type) { 'gpl-3.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('My Awesome Project')
+ expect(json_response['content']).to include('Copyright (C) 2016 Anton')
+ end
+ end
+
+ context 'for the gpl-2.0 license' do
+ let(:license_type) { 'gpl-2.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('My Awesome Project')
+ expect(json_response['content']).to include('Copyright (C) 2016 Anton')
+ end
+ end
+
+ context 'for the apache-2.0 license' do
+ let(:license_type) { 'apache-2.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('Apache License')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('Copyright 2016 Anton')
+ end
+ end
+
+ context 'for an uknown license' do
+ let(:license_type) { 'muth-over9000' }
+
+ it 'returns a 404' do
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+
+ context 'with no :fullname given' do
+ context 'with an authenticated user' do
+ let(:user) { create(:user) }
+
+ it 'replaces the copyright owner placeholder with the name of the current user' do
+ get api('/licenses/mit', user)
+
+ expect(json_response['content']).to include("Copyright (c) 2016 #{user.name}")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 9f9c3b1cf4c..edcb2bedbf7 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -32,9 +32,11 @@ describe API::API, api: true do
it "should return an array of project tags with release info" do
get api("/projects/#{project.id}/repository/tags", user)
+
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(tag_name)
+ expect(json_response.first['message']).to eq('Version 1.1.0')
expect(json_response.first['release']['description']).to eq(description)
end
end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 57d7eb927fd..dfd361a2cdd 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -20,8 +20,8 @@ describe Ci::API::API do
describe "POST /builds/register" do
it "should start a build" do
- commit = FactoryGirl.create(:ci_commit, project: project)
- commit.create_builds('master', false, nil)
+ commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
+ commit.create_builds(nil)
build = commit.builds.first
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
@@ -56,8 +56,8 @@ describe Ci::API::API do
end
it "returns options" do
- commit = FactoryGirl.create(:ci_commit, project: project)
- commit.create_builds('master', false, nil)
+ commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
+ commit.create_builds(nil)
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
@@ -66,8 +66,8 @@ describe Ci::API::API do
end
it "returns variables" do
- commit = FactoryGirl.create(:ci_commit, project: project)
- commit.create_builds('master', false, nil)
+ commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
+ commit.create_builds(nil)
project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
@@ -83,10 +83,10 @@ describe Ci::API::API do
it "returns variables for triggers" do
trigger = FactoryGirl.create(:ci_trigger, project: project)
- commit = FactoryGirl.create(:ci_commit, project: project)
+ commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger)
- commit.create_builds('master', false, nil, trigger_request)
+ commit.create_builds(nil, trigger_request)
project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
@@ -103,8 +103,8 @@ describe Ci::API::API do
end
it "returns dependent builds" do
- commit = FactoryGirl.create(:ci_commit, project: project)
- commit.create_builds('master', false, nil, nil)
+ commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
+ commit.create_builds(nil, nil)
commit.builds.where(stage: 'test').each(&:success)
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
@@ -156,6 +156,52 @@ describe Ci::API::API do
end
end
+ describe 'PATCH /builds/:id/trace.txt' do
+ let(:build) { create(:ci_build, :trace, runner_id: runner.id) }
+ let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } }
+ let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
+
+ before do
+ build.run!
+ patch ci_api("/builds/#{build.id}/trace.txt"), ' appended', headers_with_range
+ end
+
+ context 'when request is valid' do
+ it { expect(response.status).to eq 202 }
+ it { expect(build.reload.trace).to eq 'BUILD TRACE appended' }
+ it { expect(response.header).to have_key 'Range' }
+ it { expect(response.header).to have_key 'Build-Status' }
+ end
+
+ context 'when content-range start is too big' do
+ let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) }
+
+ it { expect(response.status).to eq 416 }
+ it { expect(response.header).to have_key 'Range' }
+ it { expect(response.header['Range']).to eq '0-11' }
+ end
+
+ context 'when content-range start is too small' do
+ let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) }
+
+ it { expect(response.status).to eq 416 }
+ it { expect(response.header).to have_key 'Range' }
+ it { expect(response.header['Range']).to eq '0-11' }
+ end
+
+ context 'when Content-Range header is missing' do
+ let(:headers_with_range) { headers.merge({}) }
+
+ it { expect(response.status).to eq 400 }
+ end
+
+ context 'when build has been errased' do
+ let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
+
+ it { expect(response.status).to eq 403 }
+ end
+ end
+
context "Artifacts" do
let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb
index 1fca3628686..ecc3a88a262 100644
--- a/spec/services/ci/create_builds_service_spec.rb
+++ b/spec/services/ci/create_builds_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::CreateBuildsService, services: true do
- let(:commit) { create(:ci_commit) }
+ let(:commit) { create(:ci_commit, ref: 'master') }
let(:user) { create(:user) }
describe '#execute' do
@@ -9,7 +9,7 @@ describe Ci::CreateBuildsService, services: true do
#
subject do
- described_class.new.execute(commit, 'test', 'master', nil, user, nil, status)
+ described_class.new(commit).execute(commit, nil, user, status)
end
context 'next builds available' do
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
index 870861ad20a..4cc4b3870d1 100644
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -5,7 +5,7 @@ module Ci
let(:service) { ImageForBuildService.new }
let(:project) { FactoryGirl.create(:empty_project) }
let(:commit_sha) { '01234567890123456789' }
- let(:commit) { project.ensure_ci_commit(commit_sha) }
+ let(:commit) { project.ensure_ci_commit(commit_sha, 'master') }
let(:build) { FactoryGirl.create(:ci_build, commit: commit) }
describe :execute do
diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb
index cc780587e74..a63656e6268 100644
--- a/spec/services/git_tag_push_service_spec.rb
+++ b/spec/services/git_tag_push_service_spec.rb
@@ -5,19 +5,17 @@ describe GitTagPushService, services: true do
let(:user) { create :user }
let(:project) { create :project }
- let(:service) { GitTagPushService.new }
+ let(:service) { GitTagPushService.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) }
- before do
- @oldrev = Gitlab::Git::BLANK_SHA
- @newrev = "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" # gitlab-test: git rev-parse refs/tags/v1.1.0
- @ref = 'refs/tags/v1.1.0'
- end
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+ let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
+ let(:ref) { 'refs/tags/v1.1.0' }
describe "Git Tag Push Data" do
before do
- service.execute(project, user, @oldrev, @newrev, @ref)
+ service.execute
@push_data = service.push_data
- @tag_name = Gitlab::Git.ref_name(@ref)
+ @tag_name = Gitlab::Git.ref_name(ref)
@tag = project.repository.find_tag(@tag_name)
@commit = project.commit(@tag.target)
end
@@ -25,9 +23,9 @@ describe GitTagPushService, services: true do
subject { @push_data }
it { is_expected.to include(object_kind: 'tag_push') }
- it { is_expected.to include(ref: @ref) }
- it { is_expected.to include(before: @oldrev) }
- it { is_expected.to include(after: @newrev) }
+ it { is_expected.to include(ref: ref) }
+ it { is_expected.to include(before: oldrev) }
+ it { is_expected.to include(after: newrev) }
it { is_expected.to include(message: @tag.message) }
it { is_expected.to include(user_id: user.id) }
it { is_expected.to include(user_name: user.name) }
@@ -80,9 +78,11 @@ describe GitTagPushService, services: true do
describe "Webhooks" do
context "execute webhooks" do
+ let(:service) { GitTagPushService.new(project, user, oldrev: 'oldrev', newrev: 'newrev', ref: 'refs/tags/v1.0.0') }
+
it "when pushing tags" do
expect(project).to receive(:execute_hooks)
- service.execute(project, user, 'oldrev', 'newrev', 'refs/tags/v1.0.0')
+ service.execute
end
end
end
diff --git a/spec/support/project_hook_data_shared_example.rb b/spec/support/project_hook_data_shared_example.rb
index 422083875d7..7dbaa6a6459 100644
--- a/spec/support/project_hook_data_shared_example.rb
+++ b/spec/support/project_hook_data_shared_example.rb
@@ -1,4 +1,4 @@
-RSpec.shared_examples 'project hook data' do |project_key: :project|
+RSpec.shared_examples 'project hook data with deprecateds' do |project_key: :project|
it 'contains project data' do
expect(data[project_key][:name]).to eq(project.name)
expect(data[project_key][:description]).to eq(project.description)
@@ -17,6 +17,21 @@ RSpec.shared_examples 'project hook data' do |project_key: :project|
end
end
+RSpec.shared_examples 'project hook data' do |project_key: :project|
+ it 'contains project data' do
+ expect(data[project_key][:name]).to eq(project.name)
+ expect(data[project_key][:description]).to eq(project.description)
+ expect(data[project_key][:web_url]).to eq(project.web_url)
+ expect(data[project_key][:avatar_url]).to eq(project.avatar_url)
+ expect(data[project_key][:git_http_url]).to eq(project.http_url_to_repo)
+ expect(data[project_key][:git_ssh_url]).to eq(project.ssh_url_to_repo)
+ expect(data[project_key][:namespace]).to eq(project.namespace.name)
+ expect(data[project_key][:visibility_level]).to eq(project.visibility_level)
+ expect(data[project_key][:path_with_namespace]).to eq(project.path_with_namespace)
+ expect(data[project_key][:default_branch]).to eq(project.default_branch)
+ end
+end
+
RSpec.shared_examples 'deprecated repository hook data' do |project_key: :project|
it 'contains deprecated repository data' do
expect(data[:repository][:name]).to eq(project.name)
diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb
index aa8258d6dad..73f375c481b 100644
--- a/spec/support/repo_helpers.rb
+++ b/spec/support/repo_helpers.rb
@@ -42,7 +42,7 @@ Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
eos
)
end
-
+
def another_sample_commit
OpenStruct.new(
id: "e56497bb5f03a90a51293fc6d516788730953899",
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
new file mode 100644
index 00000000000..087e4c667d8
--- /dev/null
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+require 'fileutils'
+
+describe RepositoryCheck::SingleRepositoryWorker do
+ subject { described_class.new }
+
+ it 'fails if the wiki repository is broken' do
+ project = create(:project_empty_repo, wiki_enabled: true)
+ project.create_wiki
+
+ # Test sanity: everything should be fine before the wiki repo is broken
+ subject.perform(project.id)
+ expect(project.reload.last_repository_check_failed).to eq(false)
+
+ destroy_wiki(project)
+ subject.perform(project.id)
+
+ expect(project.reload.last_repository_check_failed).to eq(true)
+ end
+
+ it 'skips wikis when disabled' do
+ project = create(:project_empty_repo, wiki_enabled: false)
+ # Make sure the test would fail if it checked the wiki repo
+ destroy_wiki(project)
+
+ subject.perform(project.id)
+
+ expect(project.reload.last_repository_check_failed).to eq(false)
+ end
+
+ def destroy_wiki(project)
+ FileUtils.rm_rf(project.wiki.repository.path_to_repo)
+ end
+end