summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc1
-rw-r--r--.gitlab-ci.yml5
-rw-r--r--CHANGELOG.md13
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock5
-rw-r--r--app/assets/javascripts/awards_handler.js79
-rw-r--r--app/assets/javascripts/behaviors/autosize.js23
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji.js17
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js5
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js3
-rw-r--r--app/assets/javascripts/build.js10
-rw-r--r--app/assets/javascripts/diff.js5
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js4
-rw-r--r--app/assets/javascripts/files_comment_button.js193
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js18
-rw-r--r--app/assets/javascripts/group_name.js23
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js26
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js24
-rw-r--r--app/assets/javascripts/main.js6
-rw-r--r--app/assets/javascripts/merge_request_tabs.js4
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring_column.vue2
-rw-r--r--app/assets/javascripts/notes.js23
-rw-r--r--app/assets/javascripts/right_sidebar.js25
-rw-r--r--app/assets/javascripts/sidebar_height_manager.js33
-rw-r--r--app/assets/javascripts/single_file_diff.js4
-rw-r--r--app/assets/javascripts/users_select.js2
-rw-r--r--app/assets/javascripts/webpack.js9
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/new_nav.scss124
-rw-r--r--app/assets/stylesheets/new_sidebar.scss7
-rw-r--r--app/assets/stylesheets/pages/diff.scss10
-rw-r--r--app/assets/stylesheets/pages/notes.scss10
-rw-r--r--app/assets/stylesheets/pages/projects.scss3
-rw-r--r--app/assets/stylesheets/pages/runners.scss17
-rw-r--r--app/controllers/groups/milestones_controller.rb3
-rw-r--r--app/controllers/projects/issues_controller.rb20
-rw-r--r--app/finders/issuable_finder.rb16
-rw-r--r--app/finders/issues_finder.rb75
-rw-r--r--app/finders/labels_finder.rb7
-rw-r--r--app/finders/users_finder.rb4
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/form_helper.rb16
-rw-r--r--app/helpers/groups_helper.rb26
-rw-r--r--app/helpers/issuables_helper.rb36
-rw-r--r--app/helpers/milestones_helper.rb2
-rw-r--r--app/helpers/notes_helper.rb12
-rw-r--r--app/helpers/projects_helper.rb12
-rw-r--r--app/helpers/search_helper.rb12
-rw-r--r--app/helpers/webpack_helper.rb23
-rw-r--r--app/models/ability.rb2
-rw-r--r--app/models/ci/build.rb7
-rw-r--r--app/models/ci/variable.rb2
-rw-r--r--app/models/concerns/issuable.rb8
-rw-r--r--app/models/concerns/mentionable/reference_regexes.rb2
-rw-r--r--app/models/concerns/routable.rb12
-rw-r--r--app/models/external_issue.rb5
-rw-r--r--app/models/forked_project_link.rb4
-rw-r--r--app/models/merge_request.rb10
-rw-r--r--app/models/project.rb25
-rw-r--r--app/models/project_services/issue_tracker_service.rb5
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/policies/base_policy.rb9
-rw-r--r--app/policies/global_policy.rb14
-rw-r--r--app/policies/user_policy.rb7
-rw-r--r--app/services/delete_merged_branches_service.rb2
-rw-r--r--app/services/git_hooks_service.rb6
-rw-r--r--app/services/git_operation_service.rb2
-rw-r--r--app/services/projects/transfer_service.rb1
-rw-r--r--app/services/quick_actions/interpret_service.rb72
-rw-r--r--app/views/admin/runners/_runner.html.haml17
-rw-r--r--app/views/dashboard/activity.html.haml1
-rw-r--r--app/views/dashboard/groups/index.html.haml1
-rw-r--r--app/views/dashboard/milestones/index.html.haml1
-rw-r--r--app/views/dashboard/projects/index.html.haml2
-rw-r--r--app/views/dashboard/snippets/index.html.haml1
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/groups/edit.html.haml15
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/layouts/_page.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml7
-rw-r--r--app/views/layouts/header/_new.html.haml2
-rw-r--r--app/views/layouts/nav/_breadcrumbs.html.haml19
-rw-r--r--app/views/layouts/nav/_project.html.haml4
-rw-r--r--app/views/profiles/preferences/show.html.haml35
-rw-r--r--app/views/projects/boards/_show.html.haml4
-rw-r--r--app/views/projects/boards/components/_sidebar.html.haml3
-rw-r--r--app/views/projects/boards/components/sidebar/_assignee.html.haml5
-rw-r--r--app/views/projects/diffs/_line.html.haml3
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml7
-rw-r--r--app/views/projects/issues/_nav_btns.html.haml11
-rw-r--r--app/views/projects/issues/index.html.haml19
-rw-r--r--app/views/projects/merge_requests/_nav_btns.html.haml5
-rw-r--r--app/views/projects/merge_requests/index.html.haml16
-rw-r--r--app/views/projects/pipelines/charts.html.haml4
-rw-r--r--app/views/projects/pipelines/charts/_overall.haml16
-rw-r--r--app/views/projects/pipelines/charts/_pipeline_times.haml2
-rw-r--r--app/views/projects/pipelines/charts/_pipelines.haml12
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/shared/_issues.html.haml2
-rw-r--r--app/views/shared/_merge_requests.html.haml2
-rw-r--r--app/views/shared/_sort_dropdown.html.haml4
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml9
-rw-r--r--app/views/shared/issuable/form/_merge_params.html.haml3
-rw-r--r--app/views/shared/issuable/form/_metadata_issue_assignee.html.haml2
-rw-r--r--app/views/shared/members/_access_request_buttons.html.haml10
-rw-r--r--changelogs/unreleased/23036-replace-dashboard-new-project-spinach.yml4
-rw-r--r--changelogs/unreleased/32048-shared-runners-admin-buttons-have-odd-spacing.yml4
-rw-r--r--changelogs/unreleased/32885-unintentionally-removing-branch-when-merging-merge-request.yml4
-rw-r--r--changelogs/unreleased/33130-remove-group-modal.yml4
-rw-r--r--changelogs/unreleased/34097-issue-board-remove-from-board-button-when-viewing-an-issue-gives-js-error-and-fails.yml4
-rw-r--r--changelogs/unreleased/34116-milestone-filtering-on-group-issues.yml4
-rw-r--r--changelogs/unreleased/34141-allow-unauthenticated-access-to-the-users-api.yml4
-rw-r--r--changelogs/unreleased/adam-external-issue-references-spike.yml4
-rw-r--r--changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml6
-rw-r--r--changelogs/unreleased/dm-dependency-linker-newlines.yml5
-rw-r--r--changelogs/unreleased/dm-empty-state-new-merge-request.yml5
-rw-r--r--changelogs/unreleased/enable-webpack-code-splitting.yml5
-rw-r--r--changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml4
-rw-r--r--changelogs/unreleased/fix-2801.yml4
-rw-r--r--changelogs/unreleased/fix-34417.yml4
-rw-r--r--changelogs/unreleased/fix-assigned-issuable-lists.yml5
-rw-r--r--changelogs/unreleased/fix-head-pipeline-for-commit-status.yml4
-rw-r--r--changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml4
-rw-r--r--changelogs/unreleased/hb-hide-archived-labels-from-group-issue-tracker.yml4
-rw-r--r--changelogs/unreleased/highest-return-on-diff-investment.yml4
-rw-r--r--changelogs/unreleased/issue-boards-closed-list-all.yml4
-rw-r--r--changelogs/unreleased/issue-form-multiple-line-markdown.yml4
-rw-r--r--changelogs/unreleased/monitoring-dashboard-fix-y-label.yml4
-rw-r--r--changelogs/unreleased/sh-allow-force-repo-create.yml4
-rw-r--r--changelogs/unreleased/sh-optimize-project-commit-api.yml4
-rw-r--r--changelogs/unreleased/speed-up-issue-counting-for-a-project.yml5
-rw-r--r--changelogs/unreleased/tc-no-delete-protected-merged-branches.yml4
-rw-r--r--config/gitlab.yml.example4
-rw-r--r--config/initializers/1_settings.rb6
-rw-r--r--config/initializers/8_metrics.rb9
-rw-r--r--config/webpack.config.js4
-rw-r--r--db/migrate/20170622135451_rename_duplicated_variable_key.rb38
-rw-r--r--db/migrate/20170622135628_add_environment_scope_to_ci_variables.rb15
-rw-r--r--db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb38
-rw-r--r--db/migrate/20170623080805_remove_ci_variables_project_id_index.rb19
-rw-r--r--db/schema.rb5
-rw-r--r--doc/api/repository_files.md6
-rw-r--r--doc/ci/docker/using_docker_images.md322
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/integration/external-issue-tracker.md3
-rw-r--r--doc/update/9.1-to-9.2.md41
-rw-r--r--doc/update/9.2-to-9.3.md9
-rw-r--r--doc/user/project/img/issue_board.pngbin76461 -> 51439 bytes
-rw-r--r--doc/user/project/img/issue_board_add_list.pngbin23632 -> 17312 bytes
-rw-r--r--doc/user/project/img/issue_board_move_issue_card_list.pngbin0 -> 74826 bytes
-rw-r--r--doc/user/project/img/issue_board_welcome_message.pngbin120751 -> 26533 bytes
-rw-r--r--doc/user/project/img/issue_boards_add_issues_modal.pngbin177057 -> 29176 bytes
-rw-r--r--doc/user/project/integrations/bugzilla.md11
-rw-r--r--doc/user/project/integrations/redmine.md11
-rw-r--r--doc/user/project/issue_board.md70
-rw-r--r--doc/user/project/issues/index.md2
-rw-r--r--doc/workflow/gitlab_flow.md2
-rw-r--r--features/dashboard/new_project.feature30
-rw-r--r--features/steps/dashboard/new_project.rb59
-rw-r--r--features/steps/dashboard/starred_projects.rb15
-rw-r--r--features/steps/shared/diff_note.rb2
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/api/projects.rb2
-rw-r--r--lib/api/users.rb26
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb15
-rw-r--r--lib/banzai/filter/external_issue_reference_filter.rb4
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb32
-rw-r--r--lib/banzai/reference_parser/issue_parser.rb3
-rw-r--r--lib/gitlab/ci/config/entry/image.rb2
-rw-r--r--lib/gitlab/ci/config/entry/service.rb4
-rw-r--r--lib/gitlab/git/hook.rb8
-rw-r--r--lib/gitlab/gitaly_client/ref.rb2
-rw-r--r--lib/gitlab/gon_helper.rb3
-rw-r--r--lib/gitlab/metrics/base_sampler.rb94
-rw-r--r--lib/gitlab/metrics/connection_rack_middleware.rb45
-rw-r--r--lib/gitlab/metrics/influx_sampler.rb (renamed from lib/gitlab/metrics/sampler.rb)40
-rw-r--r--lib/gitlab/metrics/prometheus.rb4
-rw-r--r--lib/gitlab/metrics/unicorn_sampler.rb48
-rw-r--r--lib/gitlab/shell.rb69
-rw-r--r--locale/en/gitlab.po97
-rw-r--r--locale/gitlab.pot61
-rw-r--r--package.json1
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb15
-rw-r--r--spec/factories/projects.rb2
-rw-r--r--spec/features/boards/sidebar_spec.rb16
-rw-r--r--spec/features/dashboard/projects_spec.rb16
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb2
-rw-r--r--spec/features/groups_spec.rb12
-rw-r--r--spec/features/issuables/user_sees_sidebar_spec.rb30
-rw-r--r--spec/features/issues/form_spec.rb4
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb14
-rw-r--r--spec/features/issues/move_spec.rb5
-rw-r--r--spec/features/issues/update_issues_spec.rb12
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb2
-rw-r--r--spec/features/projects/new_project_spec.rb89
-rw-r--r--spec/features/user_can_display_performance_bar_spec.rb6
-rw-r--r--spec/finders/issues_finder_spec.rb125
-rw-r--r--spec/finders/labels_finder_spec.rb6
-rw-r--r--spec/helpers/groups_helper_spec.rb2
-rw-r--r--spec/helpers/issuables_helper_spec.rb57
-rw-r--r--spec/helpers/milestones_helper_spec.rb36
-rw-r--r--spec/initializers/8_metrics_spec.rb10
-rw-r--r--spec/javascripts/awards_handler_spec.js15
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js3
-rw-r--r--spec/javascripts/monitoring/mock_data.js4
-rw-r--r--spec/javascripts/monitoring/monitoring_column_spec.js11
-rw-r--r--spec/javascripts/notes_spec.js45
-rw-r--r--spec/lib/banzai/filter/external_issue_reference_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb25
-rw-r--r--spec/lib/banzai/pipeline/gfm_pipeline_spec.rb33
-rw-r--r--spec/lib/banzai/reference_parser/issue_parser_spec.rb10
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb25
-rw-r--r--spec/lib/gitlab/ci/config/entry/image_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/entry/service_spec.rb6
-rw-r--r--spec/lib/gitlab/git/blame_spec.rb3
-rw-r--r--spec/lib/gitlab/git/branch_spec.rb2
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb2
-rw-r--r--spec/lib/gitlab/git/hook_spec.rb38
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb174
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_spec.rb20
-rw-r--r--spec/lib/gitlab/health_checks/fs_shards_check_spec.rb38
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/connection_rack_middleware_spec.rb88
-rw-r--r--spec/lib/gitlab/metrics/influx_sampler_spec.rb (renamed from spec/lib/gitlab/metrics/sampler_spec.rb)10
-rw-r--r--spec/lib/gitlab/metrics/unicorn_sampler_spec.rb108
-rw-r--r--spec/lib/gitlab/popen_spec.rb13
-rw-r--r--spec/lib/gitlab/shell_spec.rb87
-rw-r--r--spec/migrations/rename_duplicated_variable_key_spec.rb34
-rw-r--r--spec/models/ci/build_spec.rb18
-rw-r--r--spec/models/ci/variable_spec.rb12
-rw-r--r--spec/models/concerns/routable_spec.rb13
-rw-r--r--spec/models/environment_spec.rb27
-rw-r--r--spec/models/forked_project_link_spec.rb76
-rw-r--r--spec/models/merge_request_spec.rb16
-rw-r--r--spec/models/project_services/jira_service_spec.rb6
-rw-r--r--spec/models/project_services/redmine_service_spec.rb4
-rw-r--r--spec/models/project_spec.rb17
-rw-r--r--spec/models/repository_spec.rb15
-rw-r--r--spec/policies/global_policy_spec.rb34
-rw-r--r--spec/requests/api/users_spec.rb55
-rw-r--r--spec/services/delete_merged_branches_service_spec.rb8
-rw-r--r--spec/services/git_hooks_service_spec.rb7
-rw-r--r--spec/services/git_push_service_spec.rb12
-rw-r--r--spec/services/projects/transfer_service_spec.rb6
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb36
-rw-r--r--spec/support/issue_tracker_service_shared_example.rb8
-rw-r--r--spec/support/matchers/be_utf8.rb9
-rw-r--r--spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb9
-rw-r--r--yarn.lock15
252 files changed, 3201 insertions, 1326 deletions
diff --git a/.eslintrc b/.eslintrc
index 73cd7ecf66d..c72a5e0335b 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -11,6 +11,7 @@
"gon": false,
"localStorage": false
},
+ "parser": "babel-eslint",
"plugins": [
"filenames",
"import",
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e52b656599c..a3ce1de50c2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -474,9 +474,8 @@ codeclimate:
services:
- docker:dind
script:
- - docker pull codeclimate/codeclimate
- - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json
- - sed -i.bak 's/\({"body":"\)[^"]*\("}\)/\1\2/g' codeclimate.json
+ - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json
+ - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
artifacts:
paths: [codeclimate.json]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f372cbf91e8..ec768ffe9b1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,19 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 9.3.4 (2017-07-03)
+
+- No changes.
+
+## 9.3.3 (2017-06-30)
+
+- Fix head pipeline stored in merge request for external pipelines. !12478
+- Bring back branches badge to main project page. !12548
+- Fix diff of requirements.txt file by not matching newlines as part of package names.
+- Perform housekeeping only when an import of a fresh project is completed.
+- Fixed issue boards closed list not showing all closed issues.
+- Fixed multi-line markdown tooltip buttons in issue edit form.
+
## 9.3.2 (2017-06-27)
- API: Fix optional arugments for POST :id/variables. !12474
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 54d1a4f2a4a..a803cc227fe 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.13.0
+0.14.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index c20c645d7e4..ac14c3dfaa8 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-5.0.6
+5.1.1
diff --git a/Gemfile b/Gemfile
index 71d2eb1557c..323827ed01b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -285,6 +285,7 @@ group :metrics do
# Prometheus
gem 'prometheus-client-mmap', '~>0.7.0.beta5'
+ gem 'raindrops', '~> 0.18'
end
group :development do
diff --git a/Gemfile.lock b/Gemfile.lock
index f4ddd30da1b..e6ea006356c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -599,7 +599,7 @@ GEM
premailer-rails (1.9.7)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
- prometheus-client-mmap (0.7.0.beta5)
+ prometheus-client-mmap (0.7.0.beta7)
mmap2 (~> 2.2.6)
pry (0.10.4)
coderay (~> 1.1.0)
@@ -658,7 +658,7 @@ GEM
thor (>= 0.18.1, < 2.0)
rainbow (2.2.2)
rake
- raindrops (0.17.0)
+ raindrops (0.18.0)
rake (10.5.0)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
@@ -1062,6 +1062,7 @@ DEPENDENCIES
rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 4.0.9)
rainbow (~> 2.2)
+ raindrops (~> 0.18)
rblineprof (~> 0.3.6)
rdoc (~> 4.2)
recaptcha (~> 3.0)
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index c34d80f0601..18cd04b176a 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -2,7 +2,6 @@
/* global Flash */
import Cookies from 'js-cookie';
-import * as Emoji from './emoji';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
@@ -24,27 +23,9 @@ const categoryLabelMap = {
flags: 'Flags',
};
-function renderCategory(name, emojiList, opts = {}) {
- return `
- <h5 class="emoji-menu-title">
- ${name}
- </h5>
- <ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
- ${emojiList.map(emojiName => `
- <li class="emoji-menu-list-item">
- <button class="emoji-menu-btn text-center js-emoji-btn" type="button">
- ${Emoji.glEmojiTag(emojiName, {
- sprite: true,
- })}
- </button>
- </li>
- `).join('\n')}
- </ul>
- `;
-}
-
-export default class AwardsHandler {
- constructor() {
+class AwardsHandler {
+ constructor(emoji) {
+ this.emoji = emoji;
this.eventListeners = [];
// If the user shows intent let's pre-build the menu
this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
@@ -78,10 +59,10 @@ export default class AwardsHandler {
const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji');
const $spriteIconElement = $target.find('.icon');
- const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
+ const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
$target.closest('.js-awards-block').addClass('current');
- this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji);
+ this.addAward(this.getVotesBlock(), this.getAwardUrl(), emojiName);
});
}
@@ -139,16 +120,16 @@ export default class AwardsHandler {
this.isCreatingEmojiMenu = true;
// Render the first category
- const categoryMap = Emoji.getEmojiCategoryMap();
+ const categoryMap = this.emoji.getEmojiCategoryMap();
const categoryNameKey = Object.keys(categoryMap)[0];
const emojisInCategory = categoryMap[categoryNameKey];
- const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
+ const firstCategory = this.renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
// Render the frequently used
const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
let frequentlyUsedCatgegory = '';
if (frequentlyUsedEmojis.length > 0) {
- frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, {
+ frequentlyUsedCatgegory = this.renderCategory('Frequently used', frequentlyUsedEmojis, {
menuListClass: 'frequent-emojis',
});
}
@@ -179,7 +160,7 @@ export default class AwardsHandler {
}
this.isAddingRemainingEmojiMenuCategories = true;
- const categoryMap = Emoji.getEmojiCategoryMap();
+ const categoryMap = this.emoji.getEmojiCategoryMap();
// Avoid the jank and render the remaining categories separately
// This will take more time, but makes UI more responsive
@@ -191,7 +172,7 @@ export default class AwardsHandler {
promiseChain.then(() =>
new Promise((resolve) => {
const emojisInCategory = categoryMap[categoryNameKey];
- const categoryMarkup = renderCategory(
+ const categoryMarkup = this.renderCategory(
categoryLabelMap[categoryNameKey],
emojisInCategory,
);
@@ -216,6 +197,25 @@ export default class AwardsHandler {
});
}
+ renderCategory(name, emojiList, opts = {}) {
+ return `
+ <h5 class="emoji-menu-title">
+ ${name}
+ </h5>
+ <ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
+ ${emojiList.map(emojiName => `
+ <li class="emoji-menu-list-item">
+ <button class="emoji-menu-btn text-center js-emoji-btn" type="button">
+ ${this.emoji.glEmojiTag(emojiName, {
+ sprite: true,
+ })}
+ </button>
+ </li>
+ `).join('\n')}
+ </ul>
+ `;
+ }
+
positionMenu($menu, $addBtn) {
const position = $addBtn.data('position');
// The menu could potentially be off-screen or in a hidden overflow element
@@ -234,7 +234,7 @@ export default class AwardsHandler {
}
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
- const normalizedEmoji = Emoji.normalizeEmojiName(emoji);
+ const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
@@ -249,7 +249,7 @@ export default class AwardsHandler {
this.checkMutuality(votesBlock, emoji);
}
this.addEmojiToFrequentlyUsedList(emoji);
- const normalizedEmoji = Emoji.normalizeEmojiName(emoji);
+ const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
if ($emojiButton.length > 0) {
if (this.isActive($emojiButton)) {
@@ -374,7 +374,7 @@ export default class AwardsHandler {
createAwardButtonForVotesBlock(votesBlock, emojiName) {
const buttonHtml = `
<button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom">
- ${Emoji.glEmojiTag(emojiName)}
+ ${this.emoji.glEmojiTag(emojiName)}
<span class="award-control-text js-counter">1</span>
</button>
`;
@@ -440,7 +440,7 @@ export default class AwardsHandler {
}
addEmojiToFrequentlyUsedList(emoji) {
- if (Emoji.isEmojiNameValid(emoji)) {
+ if (this.emoji.isEmojiNameValid(emoji)) {
this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
}
@@ -450,7 +450,7 @@ export default class AwardsHandler {
return this.frequentlyUsedEmojis || (() => {
const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
- inputName => Emoji.isEmojiNameValid(inputName),
+ inputName => this.emoji.isEmojiNameValid(inputName),
);
return this.frequentlyUsedEmojis;
@@ -493,7 +493,7 @@ export default class AwardsHandler {
}
findMatchingEmojiElements(query) {
- const emojiMatches = Emoji.filterEmojiNamesByAlias(query);
+ const emojiMatches = this.emoji.filterEmojiNamesByAlias(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements
.filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0);
@@ -507,3 +507,12 @@ export default class AwardsHandler {
$('.emoji-menu').remove();
}
}
+
+let awardsHandlerPromise = null;
+export default function loadAwardsHandler(reload = false) {
+ if (!awardsHandlerPromise || reload) {
+ awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji')
+ .then(Emoji => new AwardsHandler(Emoji));
+ }
+ return awardsHandlerPromise;
+}
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index 3bea460dcc6..e00af4b2fa8 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,23 +1,8 @@
import autosize from 'vendor/autosize';
-$(() => {
- const $fields = $('.js-autosize');
+document.addEventListener('DOMContentLoaded', () => {
+ const autosizeEls = document.querySelectorAll('.js-autosize');
- $fields.on('autosize:resized', function resized() {
- const $field = $(this);
- $field.data('height', $field.outerHeight());
- });
-
- $fields.on('resize.autosize', function resize() {
- const $field = $(this);
- if ($field.data('height') !== $field.outerHeight()) {
- $field.data('height', $field.outerHeight());
- autosize.destroy($field);
- $field.css('max-height', window.outerHeight);
- }
- });
-
- autosize($fields);
- autosize.update($fields);
- $fields.css('resize', 'vertical');
+ autosize(autosizeEls);
+ autosize.update(autosizeEls);
});
diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js
index 8156e491a42..7e98e04303a 100644
--- a/app/assets/javascripts/behaviors/gl_emoji.js
+++ b/app/assets/javascripts/behaviors/gl_emoji.js
@@ -1,5 +1,4 @@
import installCustomElements from 'document-register-element';
-import { emojiImageTag, emojiFallbackImageSrc } from '../emoji';
import isEmojiUnicodeSupported from '../emoji/support';
installCustomElements(window);
@@ -32,11 +31,19 @@ export default function installGlEmojiElement() {
// IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass);
- } else if (hasImageFallback) {
- this.innerHTML = emojiImageTag(name, fallbackSrc);
} else {
- const src = emojiFallbackImageSrc(name);
- this.innerHTML = emojiImageTag(name, src);
+ import(/* webpackChunkName: 'emoji' */ '../emoji')
+ .then(({ emojiImageTag, emojiFallbackImageSrc }) => {
+ if (hasImageFallback) {
+ this.innerHTML = emojiImageTag(name, fallbackSrc);
+ } else {
+ const src = emojiFallbackImageSrc(name);
+ this.innerHTML = emojiImageTag(name, src);
+ }
+ })
+ .catch(() => {
+ // do nothing
+ });
}
}
};
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index c7afd4ead6b..590b7be36e3 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -34,7 +34,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({
},
milestoneTitle() {
return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
- }
+ },
+ canRemove() {
+ return !this.list.preset;
+ },
},
watch: {
detail: {
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
index 5597f128b80..6a900d4abd0 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
@@ -46,8 +46,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
},
template: `
<div
- class="block list"
- v-if="list.type !== 'closed'">
+ class="block list">
<button
class="btn btn-default btn-block"
type="button"
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 9974e135022..60103155ce0 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -85,9 +85,8 @@ window.Build = (function () {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
- });
-
- this.verifyTopPosition();
+ })
+ .then(() => this.verifyTopPosition());
}
Build.prototype.canScroll = function () {
@@ -176,7 +175,7 @@ window.Build = (function () {
}
if ($flashError.length) {
- topPostion += $flashError.outerHeight();
+ topPostion += $flashError.outerHeight() + prependTopDefault;
}
this.$buildTrace.css({
@@ -234,7 +233,8 @@ window.Build = (function () {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
- });
+ })
+ .then(() => this.verifyTopPosition());
}, 4000);
} else {
this.$buildRefreshAnimation.remove();
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 725ec7b9c70..1be9df19c81 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -1,6 +1,7 @@
/* eslint-disable class-methods-use-this */
import './lib/utils/url_utility';
+import FilesCommentButton from './files_comment_button';
const UNFOLD_COUNT = 20;
let isBound = false;
@@ -8,8 +9,10 @@ let isBound = false;
class Diff {
constructor() {
const $diffFile = $('.files .diff-file');
+
$diffFile.singleFileDiff();
- $diffFile.filesCommentButton();
+
+ FilesCommentButton.init($diffFile);
$diffFile.each((index, file) => new gl.ImageFile(file));
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
index 517bdb6be09..c37249c060a 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({
const notesCount = this.notesCount;
$(this.$el).closest('.js-avatar-container')
- .toggleClass('js-no-comment-btn', notesCount > 0)
+ .toggleClass('no-comment-btn', notesCount > 0)
.nextUntil('.js-avatar-container')
- .toggleClass('js-no-comment-btn', notesCount > 0);
+ .toggleClass('no-comment-btn', notesCount > 0);
},
toggleDiscussionsToggleState() {
const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 534e651b030..d02e4cd5876 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,150 +1,73 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
-/* global FilesCommentButton */
/* global notes */
-let $commentButtonTemplate;
-
-window.FilesCommentButton = (function() {
- var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
-
- COMMENT_BUTTON_CLASS = '.add-diff-note';
-
- LINE_HOLDER_CLASS = '.line_holder';
-
- LINE_NUMBER_CLASS = 'diff-line-num';
-
- LINE_CONTENT_CLASS = 'line_content';
-
- UNFOLDABLE_LINE_CLASS = 'js-unfold';
-
- EMPTY_CELL_CLASS = 'empty-cell';
-
- OLD_LINE_CLASS = 'old_line';
-
- LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content";
-
- TEXT_FILE_SELECTOR = '.text-file';
-
- function FilesCommentButton(filesContainerElement) {
- this.render = this.render.bind(this);
- this.hideButton = this.hideButton.bind(this);
- this.isParallelView = notes.isParallelView();
- filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render)
- .on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton);
- }
-
- FilesCommentButton.prototype.render = function(e) {
- var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
- $currentTarget = $(e.currentTarget);
-
- if ($currentTarget.hasClass('js-no-comment-btn')) return;
-
- lineContentElement = this.getLineContent($currentTarget);
- buttonParentElement = this.getButtonParent($currentTarget);
-
- if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return;
-
- $button = $(COMMENT_BUTTON_CLASS, buttonParentElement);
- buttonParentElement.addClass('is-over')
- .nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over');
-
- if ($button.length) {
- return;
+/* Developer beware! Do not add logic to showButton or hideButton
+ * that will force a reflow. Doing so will create a signficant performance
+ * bottleneck for pages with large diffs. For a comprehensive list of what
+ * causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a
+ */
+
+const LINE_NUMBER_CLASS = 'diff-line-num';
+const UNFOLDABLE_LINE_CLASS = 'js-unfold';
+const NO_COMMENT_CLASS = 'no-comment-btn';
+const EMPTY_CELL_CLASS = 'empty-cell';
+const OLD_LINE_CLASS = 'old_line';
+const LINE_COLUMN_CLASSES = `.${LINE_NUMBER_CLASS}, .line_content`;
+const DIFF_CONTAINER_SELECTOR = '.files';
+const DIFF_EXPANDED_CLASS = 'diff-expanded';
+
+export default {
+ init($diffFile) {
+ /* Caching is used only when the following members are *true*. This is because there are likely to be
+ * differently configured versions of diffs in the same session. However if these values are true, they
+ * will be true in all cases */
+
+ if (!this.userCanCreateNote) {
+ // data-can-create-note is an empty string when true, otherwise undefined
+ this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === '';
}
- textFileElement = this.getTextFileElement($currentTarget);
- buttonParentElement.append(this.buildButton({
- discussionID: lineContentElement.attr('data-discussion-id'),
- lineType: lineContentElement.attr('data-line-type'),
-
- noteableType: textFileElement.attr('data-noteable-type'),
- noteableID: textFileElement.attr('data-noteable-id'),
- commitID: textFileElement.attr('data-commit-id'),
- noteType: lineContentElement.attr('data-note-type'),
-
- // LegacyDiffNote
- lineCode: lineContentElement.attr('data-line-code'),
-
- // DiffNote
- position: lineContentElement.attr('data-position')
- }));
- };
-
- FilesCommentButton.prototype.hideButton = function(e) {
- var $currentTarget = $(e.currentTarget);
- var buttonParentElement = this.getButtonParent($currentTarget);
-
- buttonParentElement.removeClass('is-over')
- .nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over');
- };
-
- FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
- return $commentButtonTemplate.clone().attr({
- 'data-discussion-id': buttonAttributes.discussionID,
- 'data-line-type': buttonAttributes.lineType,
-
- 'data-noteable-type': buttonAttributes.noteableType,
- 'data-noteable-id': buttonAttributes.noteableID,
- 'data-commit-id': buttonAttributes.commitID,
- 'data-note-type': buttonAttributes.noteType,
-
- // LegacyDiffNote
- 'data-line-code': buttonAttributes.lineCode,
-
- // DiffNote
- 'data-position': buttonAttributes.position
- });
- };
-
- FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
- return hoveredElement.closest(TEXT_FILE_SELECTOR);
- };
-
- FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
- if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
- return hoveredElement;
- }
- if (!this.isParallelView) {
- return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
- } else {
- return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
+ if (typeof notes !== 'undefined' && !this.isParallelView) {
+ this.isParallelView = notes.isParallelView && notes.isParallelView();
}
- };
- FilesCommentButton.prototype.getButtonParent = function(hoveredElement) {
- if (!this.isParallelView) {
- if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
- return hoveredElement;
- }
- return hoveredElement.parent().find("." + OLD_LINE_CLASS);
- } else {
- if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) {
- return hoveredElement;
- }
- return $(hoveredElement).prev("." + LINE_NUMBER_CLASS);
+ if (this.userCanCreateNote) {
+ $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
+ .on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e));
}
- };
+ },
- FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
- return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS);
- };
+ showButton(isParallelView, e) {
+ const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
- FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
- return lineContentElement.attr('data-note-type') && lineContentElement.attr('data-note-type') !== '';
- };
+ if (!this.validateButtonParent(buttonParentElement)) return;
- return FilesCommentButton;
-})();
+ buttonParentElement.classList.add('is-over');
+ buttonParentElement.nextElementSibling.classList.add('is-over');
+ },
-$.fn.filesCommentButton = function() {
- $commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
+ hideButton(isParallelView, e) {
+ const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
- if (!(this && (this.parent().data('can-create-note') != null))) {
- return;
- }
- return this.each(function() {
- if (!$.data(this, 'filesCommentButton')) {
- return $.data(this, 'filesCommentButton', new FilesCommentButton($(this)));
+ buttonParentElement.classList.remove('is-over');
+ buttonParentElement.nextElementSibling.classList.remove('is-over');
+ },
+
+ getButtonParent(hoveredElement, isParallelView) {
+ if (isParallelView) {
+ if (!hoveredElement.classList.contains(LINE_NUMBER_CLASS)) {
+ return hoveredElement.previousElementSibling;
+ }
+ } else if (!hoveredElement.classList.contains(OLD_LINE_CLASS)) {
+ return hoveredElement.parentNode.querySelector(`.${OLD_LINE_CLASS}`);
}
- });
+ return hoveredElement;
+ },
+
+ validateButtonParent(buttonParentElement) {
+ return !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) &&
+ !buttonParentElement.classList.contains(UNFOLDABLE_LINE_CLASS) &&
+ !buttonParentElement.classList.contains(NO_COMMENT_CLASS) &&
+ !buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS);
+ },
};
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 10a64f9032b..2c56b718212 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -1,4 +1,3 @@
-import { validEmojiNames, glEmojiTag } from './emoji';
import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache';
@@ -373,7 +372,12 @@ class GfmAutoComplete {
if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
- this.loadData($input, at, validEmojiNames);
+ import(/* webpackChunkName: 'emoji' */ './emoji')
+ .then(({ validEmojiNames, glEmojiTag }) => {
+ this.loadData($input, at, validEmojiNames);
+ GfmAutoComplete.glEmojiTag = glEmojiTag;
+ })
+ .catch(() => { this.isLoadingData[at] = false; });
} else {
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
.then((data) => {
@@ -428,12 +432,14 @@ GfmAutoComplete.atTypeMap = {
};
// Emoji
+GfmAutoComplete.glEmojiTag = null;
GfmAutoComplete.Emoji = {
templateFunction(name) {
- return `<li>
- ${name} ${glEmojiTag(name)}
- </li>
- `;
+ // glEmojiTag helper is loaded on-demand in fetchData()
+ if (GfmAutoComplete.glEmojiTag) {
+ return `<li>${name} ${GfmAutoComplete.glEmojiTag(name)}</li>`;
+ }
+ return `<li>${name}</li>`;
},
};
// Team Members
diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js
index 462d792b8d5..37c6765d942 100644
--- a/app/assets/javascripts/group_name.js
+++ b/app/assets/javascripts/group_name.js
@@ -1,13 +1,13 @@
-
+import Cookies from 'js-cookie';
import _ from 'underscore';
export default class GroupName {
constructor() {
- this.titleContainer = document.querySelector('.title-container');
- this.title = document.querySelector('.title');
+ this.titleContainer = document.querySelector('.js-title-container');
+ this.title = this.titleContainer.querySelector('.title');
this.titleWidth = this.title.offsetWidth;
- this.groupTitle = document.querySelector('.group-title');
- this.groups = document.querySelectorAll('.group-path');
+ this.groupTitle = this.titleContainer.querySelector('.group-title');
+ this.groups = this.titleContainer.querySelectorAll('.group-path');
this.toggle = null;
this.isHidden = false;
this.init();
@@ -33,11 +33,20 @@ export default class GroupName {
createToggle() {
this.toggle = document.createElement('button');
+ this.toggle.setAttribute('type', 'button');
this.toggle.className = 'text-expander group-name-toggle';
this.toggle.setAttribute('aria-label', 'Toggle full path');
- this.toggle.innerHTML = '...';
+ if (Cookies.get('new_nav') === 'true') {
+ this.toggle.innerHTML = '<i class="fa fa-ellipsis-h" aria-hidden="true"></i>';
+ } else {
+ this.toggle.innerHTML = '...';
+ }
this.toggle.addEventListener('click', this.toggleGroups.bind(this));
- this.titleContainer.insertBefore(this.toggle, this.title);
+ if (Cookies.get('new_nav') === 'true') {
+ this.title.insertBefore(this.toggle, this.groupTitle);
+ } else {
+ this.titleContainer.insertBefore(this.toggle, this.title);
+ }
this.toggleGroups();
}
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index a8856120c5e..4f376599ba9 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -5,6 +5,7 @@
/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
+import SidebarHeightManager from './sidebar_height_manager';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
@@ -56,18 +57,6 @@ export default class IssuableBulkUpdateSidebar {
return navbarHeight + layoutNavHeight + subNavScroll;
}
- initSidebar() {
- if (!this.navHeight) {
- this.navHeight = this.getNavHeight();
- }
-
- if (!this.sidebarInitialized) {
- $(document).off('scroll').on('scroll', _.throttle(this.setSidebarHeight, 10).bind(this));
- $(window).off('resize').on('resize', _.throttle(this.setSidebarHeight, 10).bind(this));
- this.sidebarInitialized = true;
- }
- }
-
setupBulkUpdateActions() {
IssuableBulkUpdateActions.setOriginalDropdownData();
}
@@ -97,7 +86,7 @@ export default class IssuableBulkUpdateSidebar {
this.toggleCheckboxDisplay(enable);
if (enable) {
- this.initSidebar();
+ SidebarHeightManager.init();
}
}
@@ -143,17 +132,6 @@ export default class IssuableBulkUpdateSidebar {
this.$bulkEditSubmitBtn.enable();
}
}
- // loosely based on method of the same name in right_sidebar.js
- setSidebarHeight() {
- const currentScrollDepth = window.pageYOffset || 0;
- const diff = this.navHeight - currentScrollDepth;
-
- if (diff > 0) {
- this.$sidebar.outerHeight(window.innerHeight - diff);
- } else {
- this.$sidebar.outerHeight('100%');
- }
- }
static getCheckedIssueIds() {
const $checkedIssues = $('.selected_issue:checked');
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index bfcc50996cc..1d1763c3963 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -112,29 +112,11 @@ window.dateFormat = dateFormat;
return timefor;
};
- w.gl.utils.cachedTimeagoElements = [];
w.gl.utils.renderTimeago = function($els) {
- if (!$els && !w.gl.utils.cachedTimeagoElements.length) {
- w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render'));
- } else if ($els) {
- w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray());
- }
-
- w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText);
- };
-
- w.gl.utils.updateTimeagoText = function(el) {
- const formattedDate = gl.utils.getTimeago().format(el.getAttribute('datetime'), lang);
-
- if (el.textContent !== formattedDate) {
- el.textContent = formattedDate;
- }
- };
-
- w.gl.utils.initTimeagoTimeout = function() {
- gl.utils.renderTimeago();
+ const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
- gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000);
+ // timeago.js sets timeouts internally for each timeago value to be updated in real time
+ gl.utils.getTimeago().render(timeagoEls, lang);
};
w.gl.utils.getDayDifference = function(a, b) {
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index d27b4ec78c6..fe752d95b90 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -70,7 +70,7 @@ import './ajax_loading_spinner';
import './api';
import './aside';
import './autosave';
-import AwardsHandler from './awards_handler';
+import loadAwardsHandler from './awards_handler';
import './breakpoints';
import './broadcast_message';
import './build';
@@ -355,10 +355,10 @@ $(function () {
$window.off('resize.app').on('resize.app', function () {
return fitSidebarForSize();
});
- gl.awardsHandler = new AwardsHandler();
+ loadAwardsHandler();
new Aside();
- gl.utils.initTimeagoTimeout();
+ gl.utils.renderTimeago();
$(document).trigger('init.scrolling-tabs');
});
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 3cf3233cc65..7840f05a8ae 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -144,7 +144,9 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
this.resetViewContainer();
this.mountPipelinesView();
} else {
- this.expandView();
+ if (Breakpoints.get().getBreakpointSize() !== 'xs') {
+ this.expandView();
+ }
this.resetViewContainer();
this.destroyPipelinesView();
}
diff --git a/app/assets/javascripts/monitoring/components/monitoring_column.vue b/app/assets/javascripts/monitoring/components/monitoring_column.vue
index 4f4792877ee..e933634643b 100644
--- a/app/assets/javascripts/monitoring/components/monitoring_column.vue
+++ b/app/assets/javascripts/monitoring/components/monitoring_column.vue
@@ -104,7 +104,7 @@
}
this.data = query.result[0].values;
this.unitOfDisplay = query.unit || 'N/A';
- this.yAxisLabel = this.columnData.y_axis || 'Values';
+ this.yAxisLabel = this.columnData.y_label || 'Values';
this.legendTitle = query.legend || 'Average';
this.graphWidth = this.$refs.baseSvg.clientWidth -
this.margin.left - this.margin.right;
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 34476f3303f..555b8c8a65c 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -18,6 +18,7 @@ import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
import CommentTypeToggle from './comment_type_toggle';
+import loadAwardsHandler from './awards_handler';
import './autosave';
import './dropzone_input';
import './task_list';
@@ -291,8 +292,13 @@ export default class Notes {
if ('emoji_award' in noteEntity.commands_changes) {
votesBlock = $('.js-awards-block').eq(0);
- gl.awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
- return gl.awardsHandler.scrollToAwards();
+
+ loadAwardsHandler().then((awardsHandler) => {
+ awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
+ awardsHandler.scrollToAwards();
+ }).catch(() => {
+ // ignore
+ });
}
}
}
@@ -337,6 +343,10 @@ export default class Notes {
if (!noteEntity.valid) {
if (noteEntity.errors.commands_only) {
+ if (noteEntity.commands_changes &&
+ Object.keys(noteEntity.commands_changes).length > 0) {
+ $notesList.find('.system-note.being-posted').remove();
+ }
this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
this.refresh();
}
@@ -829,6 +839,8 @@ export default class Notes {
*/
setupDiscussionNoteForm(dataHolder, form) {
// setup note target
+ const diffFileData = dataHolder.closest('.text-file');
+
var discussionID = dataHolder.data('discussionId');
if (discussionID) {
@@ -839,9 +851,10 @@ export default class Notes {
form.attr('data-line-code', dataHolder.data('lineCode'));
form.find('#line_type').val(dataHolder.data('lineType'));
- form.find('#note_noteable_type').val(dataHolder.data('noteableType'));
- form.find('#note_noteable_id').val(dataHolder.data('noteableId'));
- form.find('#note_commit_id').val(dataHolder.data('commitId'));
+ form.find('#note_noteable_type').val(diffFileData.data('noteableType'));
+ form.find('#note_noteable_id').val(diffFileData.data('noteableId'));
+ form.find('#note_commit_id').val(diffFileData.data('commitId'));
+
form.find('#note_type').val(dataHolder.data('noteType'));
// LegacyDiffNote
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index b5cd01044a3..d8f1fe10b26 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */
import Cookies from 'js-cookie';
+import SidebarHeightManager from './sidebar_height_manager';
(function() {
this.Sidebar = (function() {
@@ -8,12 +9,6 @@ import Cookies from 'js-cookie';
this.toggleTodo = this.toggleTodo.bind(this);
this.sidebar = $('aside');
- this.$sidebarInner = this.sidebar.find('.issuable-sidebar');
- this.$navGitlab = $('.navbar-gitlab');
- this.$layoutNav = $('.layout-nav');
- this.$subScroll = $('.sub-nav-scroll');
- this.$rightSidebar = $('.js-right-sidebar');
-
this.removeListeners();
this.addEventListeners();
}
@@ -27,16 +22,14 @@ import Cookies from 'js-cookie';
};
Sidebar.prototype.addEventListeners = function() {
+ SidebarHeightManager.init();
const $document = $(document);
- const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20);
- const slowerThrottledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 200);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
- $(window).on('resize', () => throttledSetSidebarHeight());
- $document.on('scroll', () => slowerThrottledSetSidebarHeight());
+
$document.on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault();
@@ -214,18 +207,6 @@ import Cookies from 'js-cookie';
}
};
- Sidebar.prototype.setSidebarHeight = function() {
- const $navHeight = this.$navGitlab.outerHeight() + this.$layoutNav.outerHeight() + (this.$subScroll ? this.$subScroll.outerHeight() : 0);
- const diff = $navHeight - $(window).scrollTop();
- if (diff > 0) {
- this.$rightSidebar.outerHeight($(window).height() - diff);
- this.$sidebarInner.height('100%');
- } else {
- this.$rightSidebar.outerHeight('100%');
- this.$sidebarInner.height('');
- }
- };
-
Sidebar.prototype.isOpen = function() {
return this.sidebar.is('.right-sidebar-expanded');
};
diff --git a/app/assets/javascripts/sidebar_height_manager.js b/app/assets/javascripts/sidebar_height_manager.js
new file mode 100644
index 00000000000..022415f22b2
--- /dev/null
+++ b/app/assets/javascripts/sidebar_height_manager.js
@@ -0,0 +1,33 @@
+export default {
+ init() {
+ if (!this.initialized) {
+ this.$window = $(window);
+ this.$rightSidebar = $('.js-right-sidebar');
+ this.$navHeight = $('.navbar-gitlab').outerHeight() +
+ $('.layout-nav').outerHeight() +
+ $('.sub-nav-scroll').outerHeight();
+
+ const throttledSetSidebarHeight = _.throttle(() => this.setSidebarHeight(), 20);
+ const debouncedSetSidebarHeight = _.debounce(() => this.setSidebarHeight(), 200);
+
+ this.$window.on('scroll', throttledSetSidebarHeight);
+ this.$window.on('resize', debouncedSetSidebarHeight);
+ this.initialized = true;
+ }
+ },
+
+ setSidebarHeight() {
+ const currentScrollDepth = window.pageYOffset || 0;
+ const diff = this.$navHeight - currentScrollDepth;
+
+ if (diff > 0) {
+ const newSidebarHeight = window.innerHeight - diff;
+ this.$rightSidebar.outerHeight(newSidebarHeight);
+ this.sidebarHeightIsCustom = true;
+ } else if (this.sidebarHeightIsCustom) {
+ this.$rightSidebar.outerHeight('100%');
+ this.sidebarHeightIsCustom = false;
+ }
+ },
+};
+
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index c44892dae3d..9316a2af0b7 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,5 +1,7 @@
/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
+import FilesCommentButton from './files_comment_button';
+
(function() {
window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
@@ -78,6 +80,8 @@
gl.diffNotesCompileComponents();
}
+ FilesCommentButton.init($(_this.file));
+
if (cb) cb();
};
})(this));
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 46efdcf4202..5728afb4c59 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -206,8 +206,6 @@ function UsersSelect(currentUser, els) {
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
- var isAuthorFilter;
- isAuthorFilter = $('.js-author-search');
return _this.users(term, options, function(users) {
// GitLabDropdownFilter returns this.instance
// GitLabDropdownRemote returns this.options.instance
diff --git a/app/assets/javascripts/webpack.js b/app/assets/javascripts/webpack.js
new file mode 100644
index 00000000000..9a9cf395fb8
--- /dev/null
+++ b/app/assets/javascripts/webpack.js
@@ -0,0 +1,9 @@
+/**
+ * This is the first script loaded by webpack's runtime. It is used to manually configure
+ * config.output.publicPath to account for relative_url_root or CDN settings which cannot be
+ * baked-in to our webpack bundles.
+ */
+
+if (gon && gon.webpack_public_path) {
+ __webpack_public_path__ = gon.webpack_public_path; // eslint-disable-line
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 245117b2559..c7c2684d548 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -17,6 +17,8 @@
max-width: $limited-layout-width-sm;
margin-left: auto;
margin-right: auto;
+ padding-top: 64px;
+ padding-bottom: 64px;
}
}
diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss
index 3ce5b4fd073..bfb7a0c7e25 100644
--- a/app/assets/stylesheets/new_nav.scss
+++ b/app/assets/stylesheets/new_nav.scss
@@ -264,3 +264,127 @@ header.navbar-gitlab-new {
}
}
}
+
+.breadcrumbs {
+ display: flex;
+ min-height: 60px;
+ padding-top: $gl-padding-top;
+ padding-bottom: $gl-padding-top;
+ color: $gl-text-color;
+ border-bottom: 1px solid $border-color;
+
+ .dropdown-toggle-caret {
+ position: relative;
+ top: -1px;
+ padding: 0 5px;
+ color: rgba($black, .65);
+ font-size: 10px;
+ line-height: 1;
+ background: none;
+ border: 0;
+
+ &:focus {
+ outline: 0;
+ }
+ }
+}
+
+.breadcrumbs-container {
+ display: flex;
+ width: 100%;
+ position: relative;
+
+ .dropdown-menu-projects {
+ margin-top: -$gl-padding;
+ margin-left: $gl-padding;
+ }
+}
+
+.breadcrumbs-links {
+ flex: 1;
+ align-self: center;
+ color: $black-transparent;
+
+ a {
+ color: rgba($black, .65);
+
+ &:not(:first-child),
+ &.group-path {
+ margin-left: 4px;
+ }
+
+ &:not(:last-of-type),
+ &.group-path {
+ margin-right: 3px;
+ }
+ }
+
+ .title {
+ white-space: nowrap;
+
+ > a {
+ &:last-of-type {
+ font-weight: 600;
+ }
+ }
+ }
+
+ .avatar-tile {
+ margin-right: 5px;
+ border: 1px solid $border-color;
+ border-radius: 50%;
+ vertical-align: sub;
+
+ &.identicon {
+ float: left;
+ width: 16px;
+ height: 16px;
+ margin-top: 2px;
+ font-size: 10px;
+ }
+ }
+
+ .text-expander {
+ margin-left: 4px;
+ margin-right: 4px;
+
+ > i {
+ position: relative;
+ top: 1px;
+ }
+ }
+}
+
+.breadcrumbs-extra {
+ flex: 0 0 auto;
+ margin-left: auto;
+}
+
+.breadcrumbs-sub-title {
+ margin: 2px 0 0;
+ font-size: 16px;
+ font-weight: normal;
+
+ ul {
+ margin: 0;
+ }
+
+ li {
+ display: inline-block;
+
+ &:not(:last-child) {
+ &::after {
+ content: "/";
+ margin: 0 2px 0 5px;
+ }
+ }
+
+ &:last-child a {
+ font-weight: 600;
+ }
+ }
+
+ a {
+ color: $gl-text-color;
+ }
+}
diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss
index be4cc02b3ea..17f23f7fce3 100644
--- a/app/assets/stylesheets/new_sidebar.scss
+++ b/app/assets/stylesheets/new_sidebar.scss
@@ -49,6 +49,7 @@ $new-sidebar-width: 220px;
position: fixed;
z-index: 400;
width: $new-sidebar-width;
+ transition: width $sidebar-transition-duration;
top: 50px;
bottom: 0;
left: 0;
@@ -62,6 +63,8 @@ $new-sidebar-width: 220px;
}
li {
+ white-space: nowrap;
+
a {
display: block;
padding: 12px 14px;
@@ -72,6 +75,10 @@ $new-sidebar-width: 220px;
color: $gl-text-color;
text-decoration: none;
}
+
+ @media (max-width: $screen-xs-max) {
+ width: 0;
+ }
}
.sidebar-sub-level-items {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index b58922626fa..55011e8a21b 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -20,8 +20,6 @@
}
.diff-content {
- overflow: auto;
- overflow-y: hidden;
background: $white-light;
color: $gl-text-color;
border-radius: 0 0 3px 3px;
@@ -476,6 +474,7 @@
height: 19px;
width: 19px;
margin-left: -15px;
+ z-index: 100;
&:hover {
.diff-comment-avatar,
@@ -491,7 +490,7 @@
transform: translateX((($i * $x-pos) - $x-pos));
&:hover {
- transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2);
+ transform: translateX((($i * $x-pos) - $x-pos));
}
}
}
@@ -542,6 +541,7 @@
height: 19px;
padding: 0;
transition: transform .1s ease-out;
+ z-index: 100;
svg {
position: absolute;
@@ -555,10 +555,6 @@
fill: $white-light;
}
- &:hover {
- transform: scale(1.2);
- }
-
&:focus {
outline: 0;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 53d5cf2f7bc..303425041df 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -628,8 +628,14 @@ ul.notes {
* Line note button on the side of diffs
*/
+.line_holder .is-over:not(.no-comment-btn) {
+ .add-diff-note {
+ opacity: 1;
+ }
+}
+
.add-diff-note {
- display: none;
+ opacity: 0;
margin-top: -2px;
border-radius: 50%;
background: $white-light;
@@ -642,13 +648,11 @@ ul.notes {
width: 23px;
height: 23px;
border: 1px solid $blue-500;
- transition: transform .1s ease-in-out;
&:hover {
background: $blue-500;
border-color: $blue-600;
color: $white-light;
- transform: scale(1.15);
}
&:active {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index ba530bf7f9b..7d7c34115f9 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -483,11 +483,12 @@ a.deploy-project-label {
.project-stats {
font-size: 0;
text-align: center;
+ max-width: 100%;
+ border-bottom: 1px solid $border-color;
.nav {
padding-top: 12px;
padding-bottom: 12px;
- border-bottom: 1px solid $border-color;
}
.nav > li {
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index 9b6ff237557..57c73295d1e 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -33,3 +33,20 @@
font-weight: normal;
}
}
+
+.admin-runner-btn-group-cell {
+ min-width: 150px;
+
+ .btn-sm {
+ padding: 4px 9px;
+ }
+
+ .btn-default {
+ color: $gl-text-color-secondary;
+ }
+
+ .fa-pause,
+ .fa-play {
+ font-size: 11px;
+ }
+}
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index e52fa766044..6b1d418fc9a 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -11,6 +11,9 @@ class Groups::MilestonesController < Groups::ApplicationController
@milestone_states = GlobalMilestone.states_count(@projects)
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
+ format.json do
+ render json: milestones.map { |m| m.for_display.slice(:title, :name) }
+ end
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index dfc6baa34a4..ca483c105b6 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -267,10 +267,22 @@ class Projects::IssuesController < Projects::ApplicationController
end
def issue_params
- params.require(:issue).permit(
- :title, :assignee_id, :position, :description, :confidential,
- :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [], assignee_ids: []
- )
+ params.require(:issue).permit(*issue_params_attributes)
+ end
+
+ def issue_params_attributes
+ %i[
+ title
+ assignee_id
+ position
+ description
+ confidential
+ milestone_id
+ due_date
+ state_event
+ task_num
+ lock_version
+ ] + [{ label_ids: [], assignee_ids: [] }]
end
def authenticate_user!
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 558f8b5e2e5..7bc2117f61e 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -20,6 +20,7 @@
#
class IssuableFinder
NONE = '0'.freeze
+ IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze
attr_accessor :current_user, :params
@@ -62,7 +63,7 @@ class IssuableFinder
# grouping and counting within that query.
#
def count_by_state
- count_params = params.merge(state: nil, sort: nil)
+ count_params = params.merge(state: nil, sort: nil, for_counting: true)
labels_count = label_names.any? ? label_names.count : 1
finder = self.class.new(current_user, count_params)
counts = Hash.new(0)
@@ -86,6 +87,10 @@ class IssuableFinder
execute.find_by!(*params)
end
+ def state_counter_cache_key(state)
+ Digest::SHA1.hexdigest(state_counter_cache_key_components(state).flatten.join('-'))
+ end
+
def group
return @group if defined?(@group)
@@ -418,4 +423,13 @@ class IssuableFinder
def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end
+
+ def state_counter_cache_key_components(state)
+ opts = params.with_indifferent_access
+ opts[:state] = state
+ opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
+ opts.delete_if { |_, value| value.blank? }
+
+ ['issuables_count', klass.to_ability_name, opts.sort]
+ end
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 3da5508aefd..85230ff1293 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -16,14 +16,72 @@
# sort: string
#
class IssuesFinder < IssuableFinder
+ CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
+
def klass
Issue
end
+ def with_confidentiality_access_check
+ return Issue.all if user_can_see_all_confidential_issues?
+ return Issue.where('issues.confidential IS NOT TRUE') if user_cannot_see_confidential_issues?
+
+ Issue.where('
+ issues.confidential IS NOT TRUE
+ OR (issues.confidential = TRUE
+ AND (issues.author_id = :user_id
+ OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id)
+ OR issues.project_id IN(:project_ids)))',
+ user_id: current_user.id,
+ project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id))
+ end
+
private
def init_collection
- IssuesFinder.not_restricted_by_confidentiality(current_user)
+ with_confidentiality_access_check
+ end
+
+ def user_can_see_all_confidential_issues?
+ return @user_can_see_all_confidential_issues if defined?(@user_can_see_all_confidential_issues)
+
+ return @user_can_see_all_confidential_issues = false if current_user.blank?
+ return @user_can_see_all_confidential_issues = true if current_user.full_private_access?
+
+ @user_can_see_all_confidential_issues =
+ project? &&
+ project &&
+ project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL
+ end
+
+ # Anonymous users can't see any confidential issues.
+ #
+ # Users without access to see _all_ confidential issues (as in
+ # `user_can_see_all_confidential_issues?`) are more complicated, because they
+ # can see confidential issues where:
+ # 1. They are an assignee.
+ # 2. They are an author.
+ #
+ # That's fine for most cases, but if we're just counting, we need to cache
+ # effectively. If we cached this accurately, we'd have a cache key for every
+ # authenticated user without sufficient access to the project. Instead, when
+ # we are counting, we treat them as if they can't see any confidential issues.
+ #
+ # This does mean the counts may be wrong for those users, but avoids an
+ # explosion in cache keys.
+ def user_cannot_see_confidential_issues?(for_counting: false)
+ return false if user_can_see_all_confidential_issues?
+
+ current_user.blank? || for_counting || params[:for_counting]
+ end
+
+ def state_counter_cache_key_components(state)
+ extra_components = [
+ user_can_see_all_confidential_issues?,
+ user_cannot_see_confidential_issues?(for_counting: true)
+ ]
+
+ super + extra_components
end
def by_assignee(items)
@@ -38,21 +96,6 @@ class IssuesFinder < IssuableFinder
end
end
- def self.not_restricted_by_confidentiality(user)
- return Issue.where('issues.confidential IS NOT TRUE') if user.blank?
-
- return Issue.all if user.full_private_access?
-
- Issue.where('
- issues.confidential IS NOT TRUE
- OR (issues.confidential = TRUE
- AND (issues.author_id = :user_id
- OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id)
- OR issues.project_id IN(:project_ids)))',
- user_id: user.id,
- project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
- end
-
def item_project_ids(items)
items&.reorder(nil)&.select(:project_id)
end
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index 042d792dada..ce432ddbfe6 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -83,7 +83,12 @@ class LabelsFinder < UnionFinder
def projects
return @projects if defined?(@projects)
- @projects = skip_authorization ? Project.all : ProjectsFinder.new(current_user: current_user).execute
+ @projects = if skip_authorization
+ Project.all
+ else
+ ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute
+ end
+
@projects = @projects.in_namespace(params[:group_id]) if group?
@projects = @projects.where(id: params[:project_ids]) if projects?
@projects = @projects.reorder(nil)
diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb
index dbd50d1db7c..07deceb827b 100644
--- a/app/finders/users_finder.rb
+++ b/app/finders/users_finder.rb
@@ -60,13 +60,13 @@ class UsersFinder
end
def by_external_identity(users)
- return users unless current_user.admin? && params[:extern_uid] && params[:provider]
+ return users unless current_user&.admin? && params[:extern_uid] && params[:provider]
users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid]))
end
def by_external(users)
- return users = users.where.not(external: true) unless current_user.admin?
+ return users = users.where.not(external: true) unless current_user&.admin?
return users unless params[:external]
users.external
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 7be8e3b96cf..1c165700b19 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -298,10 +298,6 @@ module ApplicationHelper
end
end
- def can_toggle_new_nav?
- Rails.env.development?
- end
-
def show_new_nav?
cookies["new_nav"] == "true"
end
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index 8ceb5c36bda..9247b1f72de 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -16,8 +16,8 @@ module FormHelper
end
end
- def issue_dropdown_options(issuable, has_multiple_assignees = true)
- options = {
+ def issue_assignees_dropdown_options
+ {
toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data',
title: 'Select assignee',
filter: true,
@@ -27,8 +27,8 @@ module FormHelper
first_user: current_user&.username,
null_user: true,
current_user: true,
- project_id: issuable.project.try(:id),
- field_name: "#{issuable.class.model_name.param_key}[assignee_ids][]",
+ project_id: @project.id,
+ field_name: 'issue[assignee_ids][]',
default_label: 'Unassigned',
'max-select': 1,
'dropdown-header': 'Assignee',
@@ -38,13 +38,5 @@ module FormHelper
current_user_info: current_user.to_json(only: [:id, :name])
}
}
-
- if has_multiple_assignees
- options[:title] = 'Select assignee(s)'
- options[:data][:'dropdown-header'] = 'Assignee(s)'
- options[:data].delete(:'max-select')
- end
-
- options
end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index eb45241615f..8cd61f738e1 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -16,11 +16,12 @@ module GroupsHelper
full_title = ''
group.ancestors.reverse.each do |parent|
- full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable')
+ full_title += group_title_link(parent, hidable: true)
+
full_title += '<span class="hidable"> / </span>'.html_safe
end
- full_title += link_to(simple_sanitize(group.name), group_path(group), class: 'group-path')
+ full_title += group_title_link(group)
full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name
content_tag :span, class: 'group-title' do
@@ -56,4 +57,25 @@ module GroupsHelper
def group_issues(group)
IssuesFinder.new(current_user, group_id: group.id).execute
end
+
+ def remove_group_message(group)
+ _("You are going to remove %{group_name}.\nRemoved groups CANNOT be restored!\nAre you ABSOLUTELY sure?") %
+ { group_name: group.name }
+ end
+
+ private
+
+ def group_title_link(group, hidable: false)
+ link_to(group_path(group), class: "group-path #{'hidable' if hidable}") do
+ output =
+ if show_new_nav?
+ image_tag(group_icon(group), class: "avatar-tile", width: 16, height: 16)
+ else
+ ""
+ end
+
+ output << simple_sanitize(group.name)
+ output.html_safe
+ end
+ end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 3259a9c1933..05177e58c5a 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -165,11 +165,7 @@ module IssuablesHelper
}
state_title = titles[state] || state.to_s.humanize
-
- count =
- Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do
- issuables_count_for_state(issuable_type, state)
- end
+ count = issuables_count_for_state(issuable_type, state)
html = content_tag(:span, state_title)
html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
@@ -237,6 +233,18 @@ module IssuablesHelper
}
end
+ def issuables_count_for_state(issuable_type, state, finder: nil)
+ finder ||= public_send("#{issuable_type}_finder")
+ cache_key = finder.state_counter_cache_key(state)
+
+ @counts ||= {}
+ @counts[cache_key] ||= Rails.cache.fetch(cache_key, expires_in: 2.minutes) do
+ finder.count_by_state
+ end
+
+ @counts[cache_key][state]
+ end
+
private
def sidebar_gutter_collapsed?
@@ -255,24 +263,6 @@ module IssuablesHelper
end
end
- def issuables_count_for_state(issuable_type, state)
- @counts ||= {}
- @counts[issuable_type] ||= public_send("#{issuable_type}_finder").count_by_state
- @counts[issuable_type][state]
- end
-
- IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze
- private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY
-
- def issuables_state_counter_cache_key(issuable_type, state)
- opts = params.with_indifferent_access
- opts[:state] = state
- opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
- opts.delete_if { |_, value| value.blank? }
-
- hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-'))
- end
-
def issuable_templates(issuable)
@issuable_templates ||=
case issuable
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index a230db22fa2..f346e20e807 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -74,6 +74,8 @@ module MilestonesHelper
project = @target_project || @project
if project
namespace_project_milestones_path(project.namespace, project, :json)
+ elsif @group
+ group_milestones_path(@group, :json)
else
dashboard_milestones_path(:json)
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 64ad7b280cb..ecc6cd6c6c5 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -47,6 +47,18 @@ module NotesHelper
data
end
+ def add_diff_note_button(line_code, position, line_type)
+ return if @diff_notes_disabled
+
+ button_tag '',
+ class: 'add-diff-note js-add-diff-note-button',
+ type: 'submit', name: 'button',
+ data: diff_view_line_data(line_code, position, line_type),
+ title: 'Add a comment to this line' do
+ icon('comment-o')
+ end
+ end
+
def link_to_reply_discussion(discussion, line_type = nil)
return unless current_user
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index c04b1419a19..53d95c2de94 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -58,7 +58,17 @@ module ProjectsHelper
link_to(simple_sanitize(owner.name), user_path(owner))
end
- project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" }
+ project_link = link_to project_path(project), { class: "project-item-select-holder" } do
+ output =
+ if show_new_nav?
+ project_icon(project, alt: project.name, class: 'avatar-tile', width: 16, height: 16)
+ else
+ ""
+ end
+
+ output << simple_sanitize(project.name)
+ output.html_safe
+ end
if current_user
project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 8f15904f068..f39a3bb55a8 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -126,6 +126,18 @@ module SearchHelper
search_path(options)
end
+ def search_filter_input_options(type)
+ {
+ id: "filtered-search-#{type}",
+ placeholder: 'Search or filter results...',
+ data: {
+ 'project-id' => @project.id,
+ 'username-params' => @users.to_json(only: [:id, :username]),
+ 'base-endpoint' => namespace_project_path(@project.namespace, @project)
+ }
+ }
+ end
+
# Sanitize a HTML field for search display. Most tags are stripped out and the
# maximum length is set to 200 characters.
def search_md_sanitize(object, field)
diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb
index 6bacda9fe75..0386df22374 100644
--- a/app/helpers/webpack_helper.rb
+++ b/app/helpers/webpack_helper.rb
@@ -11,20 +11,29 @@ module WebpackHelper
paths = Webpack::Rails::Manifest.asset_paths(source)
if extension
- paths = paths.select { |p| p.ends_with? ".#{extension}" }
+ paths.select! { |p| p.ends_with? ".#{extension}" }
end
- # include full webpack-dev-server url for rspec tests running locally
+ force_host = webpack_public_host
+ if force_host
+ paths.map! { |p| "#{force_host}#{p}" }
+ end
+
+ paths
+ end
+
+ def webpack_public_host
if Rails.env.test? && Rails.configuration.webpack.dev_server.enabled
host = Rails.configuration.webpack.dev_server.host
port = Rails.configuration.webpack.dev_server.port
protocol = Rails.configuration.webpack.dev_server.https ? 'https' : 'http'
-
- paths.map! do |p|
- "#{protocol}://#{host}:#{port}#{p}"
- end
+ "#{protocol}://#{host}:#{port}"
+ else
+ ActionController::Base.asset_host.try(:chomp, '/')
end
+ end
- paths
+ def webpack_public_path
+ "#{webpack_public_host}/#{Rails.application.config.webpack.public_path}/"
end
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index d2b8a8447b5..0b6bcbde5d9 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -1,4 +1,4 @@
-require 'declarative_policy'
+require_dependency 'declarative_policy'
class Ability
class << self
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index a300536532b..2e7a80d308b 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -176,9 +176,12 @@ module Ci
# * Lowercased
# * Anything not matching [a-z0-9-] is replaced with a -
# * Maximum length is 63 bytes
+ # * First/Last Character is not a hyphen
def ref_slug
- slugified = ref.to_s.downcase
- slugified.gsub(/[^a-z0-9]/, '-')[0..62]
+ ref.to_s
+ .downcase
+ .gsub(/[^a-z0-9]/, '-')[0..62]
+ .gsub(/(\A-+|-+\z)/, '')
end
# Variables whose value does not depend on environment
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 96d6e120998..0b8d0ff881a 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -5,7 +5,7 @@ module Ci
belongs_to :project
- validates :key, uniqueness: { scope: :project_id }
+ validates :key, uniqueness: { scope: [:project_id, :environment_scope] }
scope :unprotected, -> { where(protected: false) }
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index d178ee4422b..41c8b525273 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -102,6 +102,14 @@ module Issuable
def locking_enabled?
title_changed? || description_changed?
end
+
+ def allows_multiple_assignees?
+ false
+ end
+
+ def has_multiple_assignees?
+ assignees.count > 1
+ end
end
module ClassMethods
diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb
index 1848230ec7e..2d86a70c395 100644
--- a/app/models/concerns/mentionable/reference_regexes.rb
+++ b/app/models/concerns/mentionable/reference_regexes.rb
@@ -14,7 +14,7 @@ module Mentionable
end
EXTERNAL_PATTERN = begin
- issue_pattern = ExternalIssue.reference_pattern
+ issue_pattern = IssueTrackerService.reference_pattern
link_patterns = URI.regexp(%w(http https))
reference_pattern(link_patterns, issue_pattern)
end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index ec7796a9dbb..ee108f010a6 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -103,8 +103,12 @@ module Routable
def full_path
return uncached_full_path unless RequestStore.active?
- key = "routable/full_path/#{self.class.name}/#{self.id}"
- RequestStore[key] ||= uncached_full_path
+ RequestStore[full_path_key] ||= uncached_full_path
+ end
+
+ def expires_full_path_cache
+ RequestStore.delete(full_path_key) if RequestStore.active?
+ @full_path = nil
end
def build_full_path
@@ -135,6 +139,10 @@ module Routable
path_changed? || parent_changed?
end
+ def full_path_key
+ @full_path_key ||= "routable/full_path/#{self.class.name}/#{self.id}"
+ end
+
def build_full_name
if parent && name
parent.human_name + ' / ' + name
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index e63f89a9f85..0bf18e529f0 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -38,11 +38,6 @@ class ExternalIssue
@project.id
end
- # Pattern used to extract `JIRA-123` issue references from text
- def self.reference_pattern
- @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
- end
-
def to_reference(_from_project = nil, full: nil)
id
end
diff --git a/app/models/forked_project_link.rb b/app/models/forked_project_link.rb
index 36cf7ad6a28..8d35864eff6 100644
--- a/app/models/forked_project_link.rb
+++ b/app/models/forked_project_link.rb
@@ -1,4 +1,4 @@
class ForkedProjectLink < ActiveRecord::Base
- belongs_to :forked_to_project, class_name: 'Project'
- belongs_to :forked_from_project, class_name: 'Project'
+ belongs_to :forked_to_project, -> { where.not(pending_delete: true) }, class_name: 'Project'
+ belongs_to :forked_from_project, -> { where.not(pending_delete: true) }, class_name: 'Project'
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index c099d731082..808212c780c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -197,11 +197,19 @@ class MergeRequest < ActiveRecord::Base
}
end
- # This method is needed for compatibility with issues to not mess view and other code
+ # These method are needed for compatibility with issues to not mess view and other code
def assignees
Array(assignee)
end
+ def assignee_ids
+ Array(assignee_id)
+ end
+
+ def assignee_ids=(ids)
+ write_attribute(:assignee_id, ids.last)
+ end
+
def assignee_or_author?(user)
author_id == user.id || assignee_id == user.id
end
diff --git a/app/models/project.rb b/app/models/project.rb
index a75c5209955..241e7e60dd2 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -727,8 +727,8 @@ class Project < ActiveRecord::Base
end
end
- def issue_reference_pattern
- issues_tracker.reference_pattern
+ def external_issue_reference_pattern
+ external_issue_tracker.class.reference_pattern
end
def default_issues_tracker?
@@ -963,6 +963,7 @@ class Project < ActiveRecord::Base
begin
gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
send_move_instructions(old_path_with_namespace)
+ expires_full_path_cache
@old_path_with_namespace = old_path_with_namespace
@@ -1073,21 +1074,21 @@ class Project < ActiveRecord::Base
merge_requests.where(source_project_id: self.id)
end
- def create_repository
+ def create_repository(force: false)
# Forked import is handled asynchronously
- unless forked?
- if gitlab_shell.add_repository(repository_storage_path, path_with_namespace)
- repository.after_create
- true
- else
- errors.add(:base, 'Failed to create repository via gitlab-shell')
- false
- end
+ return if forked? && !force
+
+ if gitlab_shell.add_repository(repository_storage_path, path_with_namespace)
+ repository.after_create
+ true
+ else
+ errors.add(:base, 'Failed to create repository via gitlab-shell')
+ false
end
end
def ensure_repository
- create_repository unless repository_exists?
+ create_repository(force: true) unless repository_exists?
end
def repository_exists?
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index ff138b9066d..fcc7c4bec06 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -5,7 +5,10 @@ class IssueTrackerService < Service
# Pattern used to extract links from comments
# Override this method on services that uses different patterns
- def reference_pattern
+ # This pattern does not support cross-project references
+ # The other code assumes that this pattern is a superset of all
+ # overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN
+ def self.reference_pattern
@reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)}
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 2450fb43212..00328892b4a 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -18,7 +18,7 @@ class JiraService < IssueTrackerService
end
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
- def reference_pattern
+ def self.reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 00067ce756e..a605a3457c8 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -1,6 +1,8 @@
-require 'declarative_policy'
+require_dependency 'declarative_policy'
class BasePolicy < DeclarativePolicy::Base
+ include Gitlab::CurrentSettings
+
desc "User is an instance admin"
with_options scope: :user, score: 0
condition(:admin) { @user&.admin? }
@@ -10,4 +12,9 @@ class BasePolicy < DeclarativePolicy::Base
with_options scope: :user, score: 0
condition(:can_create_group) { @user&.can_create_group }
+
+ desc "The application is restricted from public visibility"
+ condition(:restricted_public_level, scope: :global) do
+ current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
+ end
end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 535faa922dd..55eefa76d3f 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -11,10 +11,16 @@ class GlobalPolicy < BasePolicy
with_options scope: :user, score: 0
condition(:access_locked) { @user.access_locked? }
- rule { anonymous }.prevent_all
+ rule { anonymous }.policy do
+ prevent :log_in
+ prevent :access_api
+ prevent :access_git
+ prevent :receive_notifications
+ prevent :use_quick_actions
+ prevent :create_group
+ end
rule { default }.policy do
- enable :read_users_list
enable :log_in
enable :access_api
enable :access_git
@@ -37,4 +43,8 @@ class GlobalPolicy < BasePolicy
rule { access_locked }.policy do
prevent :log_in
end
+
+ rule { ~restricted_public_level }.policy do
+ enable :read_users_list
+ end
end
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 0181ddf85e0..0905ddd9b38 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -1,11 +1,4 @@
class UserPolicy < BasePolicy
- include Gitlab::CurrentSettings
-
- desc "The application is restricted from public visibility"
- condition(:restricted_public_level, scope: :global) do
- current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
- end
-
desc "The current user is the user in question"
condition(:user_is_self, score: 0) { @subject == @user }
diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb
index 3b611588466..5c9e2a16c71 100644
--- a/app/services/delete_merged_branches_service.rb
+++ b/app/services/delete_merged_branches_service.rb
@@ -10,6 +10,8 @@ class DeleteMergedBranchesService < BaseService
branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
# Prevent deletion of branches relevant to open merge requests
branches -= merge_request_branch_names
+ # Prevent deletion of protected branches
+ branches -= project.protected_branches.pluck(:name)
branches.each do |branch|
DeleteBranchService.new(project, current_user).execute(branch)
diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb
index d222d1e63aa..eab65d09299 100644
--- a/app/services/git_hooks_service.rb
+++ b/app/services/git_hooks_service.rb
@@ -3,8 +3,8 @@ class GitHooksService
attr_accessor :oldrev, :newrev, :ref
- def execute(user, repo_path, oldrev, newrev, ref)
- @repo_path = repo_path
+ def execute(user, project, oldrev, newrev, ref)
+ @project = project
@user = Gitlab::GlId.gl_id(user)
@oldrev = oldrev
@newrev = newrev
@@ -26,7 +26,7 @@ class GitHooksService
private
def run_hook(name)
- hook = Gitlab::Git::Hook.new(name, @repo_path)
+ hook = Gitlab::Git::Hook.new(name, @project)
hook.trigger(@user, oldrev, newrev, ref)
end
end
diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb
index ed6ea638235..43636fde0be 100644
--- a/app/services/git_operation_service.rb
+++ b/app/services/git_operation_service.rb
@@ -120,7 +120,7 @@ class GitOperationService
def with_hooks(ref, newrev, oldrev)
GitHooksService.new.execute(
user,
- repository.path_to_repo,
+ repository.project,
oldrev,
newrev,
ref) do |service|
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index fd701e33524..4bb98e5cb4e 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -78,6 +78,7 @@ module Projects
Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
project.old_path_with_namespace = @old_path
+ project.expires_full_path_cache
execute_system_hooks
end
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 6816b137361..e4dfe87e614 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -92,9 +92,12 @@ module QuickActions
desc 'Assign'
explanation do |users|
- "Assigns #{users.first.to_reference}." if users.any?
+ users = issuable.allows_multiple_assignees? ? users : users.take(1)
+ "Assigns #{users.map(&:to_reference).to_sentence}."
+ end
+ params do
+ issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user'
end
- params '@user'
condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
@@ -104,28 +107,69 @@ module QuickActions
command :assign do |users|
next if users.empty?
- if issuable.is_a?(Issue)
- @updates[:assignee_ids] = [users.last.id]
+ @updates[:assignee_ids] =
+ if issuable.allows_multiple_assignees?
+ issuable.assignees.pluck(:id) + users.map(&:id)
+ else
+ [users.last.id]
+ end
+ end
+
+ desc do
+ if issuable.allows_multiple_assignees?
+ 'Remove all or specific assignee(s)'
else
- @updates[:assignee_id] = users.last.id
+ 'Remove assignee'
end
end
-
- desc 'Remove assignee'
explanation do
- "Removes assignee #{issuable.assignees.first.to_reference}."
+ "Removes #{'assignee'.pluralize(issuable.assignees.size)} #{issuable.assignees.map(&:to_reference).to_sentence}."
+ end
+ params do
+ issuable.allows_multiple_assignees? ? '@user1 @user2' : ''
end
condition do
issuable.persisted? &&
issuable.assignees.any? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
- command :unassign do
- if issuable.is_a?(Issue)
- @updates[:assignee_ids] = []
- else
- @updates[:assignee_id] = nil
- end
+ parse_params do |unassign_param|
+ # When multiple users are assigned, all will be unassigned if multiple assignees are no longer allowed
+ extract_users(unassign_param) if issuable.allows_multiple_assignees?
+ end
+ command :unassign do |users = nil|
+ @updates[:assignee_ids] =
+ if users&.any?
+ issuable.assignees.pluck(:id) - users.map(&:id)
+ else
+ []
+ end
+ end
+
+ desc do
+ "Change assignee#{'(s)' if issuable.allows_multiple_assignees?}"
+ end
+ explanation do |users|
+ users = issuable.allows_multiple_assignees? ? users : users.take(1)
+ "Change #{'assignee'.pluralize(users.size)} to #{users.map(&:to_reference).to_sentence}."
+ end
+ params do
+ issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user'
+ end
+ condition do
+ issuable.persisted? &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ parse_params do |assignee_param|
+ extract_users(assignee_param)
+ end
+ command :reassign do |users|
+ @updates[:assignee_ids] =
+ if issuable.allows_multiple_assignees?
+ users.map(&:id)
+ else
+ [users.last.id]
+ end
end
desc 'Set milestone'
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index d4d166ab7b6..140688b52d3 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -32,13 +32,16 @@
#{time_ago_in_words(runner.contacted_at)} ago
- else
Never
- %td
- .pull-right
- = link_to 'Edit', admin_runner_path(runner), class: 'btn btn-sm'
+ %td.admin-runner-btn-group-cell
+ .pull-right.btn-group
+ = link_to admin_runner_path(runner), class: 'btn btn-sm btn-default has-tooltip', title: 'Edit', ref: 'tooltip', aria: { label: 'Edit' }, data: { placement: 'top', container: 'body'} do
+ = icon('pencil')
&nbsp;
- if runner.active?
- = link_to 'Pause', [:pause, :admin, runner], data: { confirm: "Are you sure?" }, method: :get, class: 'btn btn-danger btn-sm'
+ = link_to [:pause, :admin, runner], method: :get, class: 'btn btn-sm btn-default has-tooltip', title: 'Pause', ref: 'tooltip', aria: { label: 'Pause' }, data: { placement: 'top', container: 'body', confirm: "Are you sure?" } do
+ = icon('pause')
- else
- = link_to 'Resume', [:resume, :admin, runner], method: :get, class: 'btn btn-success btn-sm'
- = link_to 'Remove', [:admin, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
-
+ = link_to [:resume, :admin, runner], method: :get, class: 'btn btn-default btn-sm has-tooltip', title: 'Resume', ref: 'tooltip', aria: { label: 'Resume' }, data: { placement: 'top', container: 'body'} do
+ = icon('play')
+ = link_to [:admin, runner], method: :delete, class: 'btn btn-danger btn-sm has-tooltip', title: 'Remove', ref: 'tooltip', aria: { label: 'Remove' }, data: { placement: 'top', container: 'body', confirm: "Are you sure?" } do
+ = icon('remove')
diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml
index f893c3e1675..ad35d05c29a 100644
--- a/app/views/dashboard/activity.html.haml
+++ b/app/views/dashboard/activity.html.haml
@@ -1,3 +1,4 @@
+- @hide_top_links = true
- @no_container = true
= content_for :meta_tags do
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index f9b45a539a1..1cea8182733 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -1,3 +1,4 @@
+- @hide_top_links = true
- page_title "Groups"
- header_title "Groups", dashboard_groups_path
= render 'dashboard/groups_head'
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index 664ec618b79..ef1467c4d78 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -1,3 +1,4 @@
+- @hide_top_links = true
- page_title 'Milestones'
- header_title 'Milestones', dashboard_milestones_path
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 5e63a61e21b..7ac6cf06fb9 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -1,4 +1,6 @@
- @no_container = true
+- @hide_top_links = true
+- @breadcrumb_title = "Projects"
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index 85cbe0bf0e6..e86b1ab3116 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -1,3 +1,4 @@
+- @hide_top_links = true
- page_title "Snippets"
- header_title "Snippets", dashboard_snippets_path
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index d696577278d..298604dee8c 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -25,7 +25,7 @@
%div
- if Gitlab::Recaptcha.enabled?
= recaptcha_tags
- %div
+ .submit-container
= f.submit "Register", class: "btn-register btn"
.clearfix.submit-container
%p
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 7d5add3cc1c..9ebb3894c55 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -45,10 +45,13 @@
.panel.panel-danger
.panel-heading Remove group
.panel-body
- %p
- Removing group will cause all child projects and resources to be removed.
- %br
- %strong Removed group can not be restored!
+ = form_tag(@group, method: :delete) do
+ %p
+ Removing group will cause all child projects and resources to be removed.
+ %br
+ %strong Removed group can not be restored!
- .form-actions
- = link_to 'Remove group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove"
+ .form-actions
+ = button_to 'Remove group', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_group_message(@group) }
+
+= render 'shared/confirm_modal', phrase: @group.path
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index cc710f4ec7d..abb6dc2e9f3 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -38,7 +38,7 @@
= Gon::Base.render_data
- = webpack_bundle_tag "runtime"
+ = webpack_bundle_tag "webpack_runtime"
= webpack_bundle_tag "common"
= webpack_bundle_tag "locale"
= webpack_bundle_tag "main"
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 62a76a1b00e..1a9f5401a78 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -14,6 +14,8 @@
= render "layouts/broadcast"
= render "layouts/flash"
= yield :flash_message
+ - if show_new_nav?
+ = render "layouts/nav/breadcrumbs"
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
.content{ id: "content-body" }
= yield
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index f056c0af968..7e8b9cb9ad0 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -17,7 +17,7 @@
= link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
= brand_header_logo
- .title-container
+ .title-container.js-title-container
%h1.title{ class: ('initializing' if @has_group_title) }= title
.navbar-collapse.collapse
@@ -74,9 +74,8 @@
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li
= link_to "Settings", profile_path
- - if can_toggle_new_nav?
- %li
- = link_to "Turn on new nav", profile_preferences_path(anchor: "new-navigation")
+ %li
+ = link_to "Turn on new nav", profile_preferences_path(anchor: "new-navigation")
%li.divider
%li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link"
diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml
index c0833c64911..5859f689dd1 100644
--- a/app/views/layouts/header/_new.html.haml
+++ b/app/views/layouts/header/_new.html.haml
@@ -83,8 +83,6 @@
= icon('ellipsis-v', class: 'js-navbar-toggle-right')
= icon('times', class: 'js-navbar-toggle-left', style: 'display: none;')
- = yield :header_content
-
= render 'shared/outdated_browser'
- if @project && !@project.empty_repo?
diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml
new file mode 100644
index 00000000000..5f1641f4300
--- /dev/null
+++ b/app/views/layouts/nav/_breadcrumbs.html.haml
@@ -0,0 +1,19 @@
+- breadcrumb_title = @breadcrumb_title || controller.controller_name.humanize
+- hide_top_links = @hide_top_links || false
+
+%nav.breadcrumbs{ role: "navigation" }
+ .breadcrumbs-container{ class: container_class }
+ .breadcrumbs-links.js-title-container
+ - unless hide_top_links
+ .title
+ = link_to "GitLab", root_path
+ \/
+ = header_title
+ %h2.breadcrumbs-sub-title
+ %ul.list-unstyled
+ - if content_for?(:sub_title_before)
+ = yield :sub_title_before
+ %li= link_to breadcrumb_title, request.path
+ - if content_for?(:breadcrumbs_extra)
+ .breadcrumbs-extra.hidden-xs= yield :breadcrumbs_extra
+ = yield :header_content
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 68024d782a6..b095adcfe7e 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -28,7 +28,7 @@
%span
Issues
- if @project.default_issues_tracker?
- %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
+ %span.badge.count.issue_counter= number_with_delimiter(issuables_count_for_state(:issues, :opened, finder: IssuesFinder.new(current_user, project_id: @project.id)))
- if project_nav_tab? :merge_requests
- controllers = [:merge_requests, 'projects/merge_requests/conflicts']
@@ -37,7 +37,7 @@
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span
Merge Requests
- %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
+ %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(issuables_count_for_state(:merge_requests, :opened, finder: MergeRequestsFinder.new(current_user, project_id: @project.id)))
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index a089aeb2447..2dd938f8379 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -16,25 +16,22 @@
.preview= image_tag "#{scheme.css_class}-scheme-preview.png"
= f.radio_button :color_scheme_id, scheme.id
= scheme.name
- - if can_toggle_new_nav?
- .col-sm-12
- %hr
- .col-lg-3.profile-settings-sidebar#new-navigation
- %h4.prepend-top-0
- New Navigation
- %p
- This setting allows you to turn on or off the new upcoming navigation concept.
- = succeed '.' do
- = link_to 'Learn more', '', target: '_blank'
- .col-lg-9.syntax-theme
- = label_tag do
- .preview= image_tag "old_nav.png"
- %input.js-experiment-feature-toggle{ type: "radio", value: "false", name: "new_nav", checked: !show_new_nav? }
- Old
- = label_tag do
- .preview= image_tag "new_nav.png"
- %input.js-experiment-feature-toggle{ type: "radio", value: "true", name: "new_nav", checked: show_new_nav? }
- New
+ .col-sm-12
+ %hr
+ .col-lg-3.profile-settings-sidebar#new-navigation
+ %h4.prepend-top-0
+ New Navigation
+ %p
+ This setting allows you to turn on or off the new upcoming navigation concept.
+ .col-lg-9.syntax-theme
+ = label_tag do
+ .preview= image_tag "old_nav.png"
+ %input.js-experiment-feature-toggle{ type: "radio", value: "false", name: "new_nav", checked: !show_new_nav? }
+ Old
+ = label_tag do
+ .preview= image_tag "new_nav.png"
+ %input.js-experiment-feature-toggle{ type: "radio", value: "true", name: "new_nav", checked: show_new_nav? }
+ New
.col-sm-12
%hr
.col-lg-4.profile-settings-sidebar
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
index 6684ecfce81..3622720a8b7 100644
--- a/app/views/projects/boards/_show.html.haml
+++ b/app/views/projects/boards/_show.html.haml
@@ -2,6 +2,10 @@
- @content_class = "issue-boards-content"
- page_title "Boards"
+- if show_new_nav?
+ - content_for :sub_title_before do
+ %li= link_to "Issues", namespace_project_issues_path(@project.namespace, @project)
+
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml
index 24d76da6f06..09d70f658a3 100644
--- a/app/views/projects/boards/components/_sidebar.html.haml
+++ b/app/views/projects/boards/components/_sidebar.html.haml
@@ -23,4 +23,5 @@
= render "projects/boards/components/sidebar/labels"
= render "projects/boards/components/sidebar/notifications"
%remove-btn{ ":issue" => "issue",
- ":list" => "list" }
+ ":list" => "list",
+ "v-if" => "canRemove" }
diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml
index e8db868f49b..c314573bdea 100644
--- a/app/views/projects/boards/components/sidebar/_assignee.html.haml
+++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml
@@ -19,10 +19,11 @@
":data-name" => "assignee.name",
":data-username" => "assignee.username" }
.dropdown
- %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: "button", ref: "assigneeDropdown", data: { toggle: "dropdown", field_name: "issue[assignee_ids][]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", multi_select: "true", 'max-select' => 1, dropdown: { header: 'Assignee' } },
+ - dropdown_options = issue_assignees_dropdown_options
+ %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: { toggle: 'dropdown', field_name: 'issue[assignee_ids][]', first_user: current_user&.username, current_user: 'true', project_id: @project.id, null_user: 'true', multi_select: 'true', 'dropdown-header': dropdown_options[:data][:'dropdown-header'], 'max-select': dropdown_options[:data][:'max-select'] },
":data-issuable-id" => "issue.id",
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
- Select assignee
+ = dropdown_options[:title]
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
= dropdown_title("Assign to")
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 43708d22a0c..cd0fb21f8a7 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -19,6 +19,7 @@
- if plain
= link_text
- else
+ = add_diff_note_button(line_code, diff_file.position(line), type)
%a{ href: "##{line_code}", data: { linenumber: link_text } }
- discussion = line_discussions.try(:first)
- if discussion && discussion.resolvable? && !plain
@@ -29,7 +30,7 @@
= link_text
- else
%a{ href: "##{line_code}", data: { linenumber: link_text } }
- %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }<
+ %td.line_content.noteable_line{ class: type }<
- if email
%pre= line.text
- else
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 8e5f4d2573d..56d63250714 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -1,4 +1,5 @@
/ Side-by-side diff view
+
.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data }
%table
- diff_file.parallel_diff_lines.each do |line|
@@ -18,11 +19,12 @@
- left_line_code = diff_file.line_code(left)
- left_position = diff_file.position(left)
%td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
+ = add_diff_note_button(left_line_code, left_position, 'old')
%a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
- discussion_left = discussions_left.try(:first)
- if discussion_left && discussion_left.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_left.id }
- %td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text)
+ %td.line_content.parallel.noteable_line{ class: left.type }= diff_line_content(left.text)
- else
%td.old_line.diff-line-num.empty-cell
%td.line_content.parallel
@@ -38,11 +40,12 @@
- right_line_code = diff_file.line_code(right)
- right_position = diff_file.position(right)
%td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
+ = add_diff_note_button(right_line_code, right_position, 'new')
%a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
- discussion_right = discussions_right.try(:first)
- if discussion_right && discussion_right.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_right.id }
- %td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text)
+ %td.line_content.parallel.noteable_line{ class: right.type }= diff_line_content(right.text)
- else
%td.old_line.diff-line-num.empty-cell
%td.line_content.parallel
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml
new file mode 100644
index 00000000000..698959ec74f
--- /dev/null
+++ b/app/views/projects/issues/_nav_btns.html.haml
@@ -0,0 +1,11 @@
+= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
+ = icon('rss')
+- if @can_bulk_update
+ = button_tag "Edit Issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
+= link_to "New issue", new_namespace_project_issue_path(@project.namespace,
+ @project,
+ issue: { assignee_id: issues_finder.assignee.try(:id),
+ milestone_id: issues_finder.milestones.first.try(:id) }),
+ class: "btn btn-new",
+ title: "New issue",
+ id: "new_issue_link"
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 7183794ce72..89ac5ff7128 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -13,23 +13,16 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
+- if show_new_nav?
+ - content_for :breadcrumbs_extra do
+ = render "projects/issues/nav_btns"
+
- if project_issues(@project).exists?
%div{ class: (container_class) }
.top-area
= render 'shared/issuable/nav', type: :issues
- .nav-controls
- = link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do
- = icon('rss')
- - if @can_bulk_update
- = button_tag "Edit Issues", class: "btn btn-default js-bulk-update-toggle"
- = link_to new_namespace_project_issue_path(@project.namespace,
- @project,
- issue: { assignee_id: issues_finder.assignee.try(:id),
- milestone_id: issues_finder.milestones.first.try(:id) }),
- class: "btn btn-new",
- title: "New issue",
- id: "new_issue_link" do
- New issue
+ .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ = render "projects/issues/nav_btns"
= render 'shared/issuable/search_bar', type: :issues
- if @can_bulk_update
diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml
new file mode 100644
index 00000000000..e92f2712347
--- /dev/null
+++ b/app/views/projects/merge_requests/_nav_btns.html.haml
@@ -0,0 +1,5 @@
+- if @can_bulk_update
+ = button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle"
+- if merge_project
+ = link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do
+ New merge request
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 86996e488a1..6fe44ba3c3d 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -1,5 +1,7 @@
- @no_container = true
- @can_bulk_update = can?(current_user, :admin_merge_request, @project)
+- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+- new_merge_request_path = namespace_project_new_merge_request_path(merge_project.namespace, merge_project) if merge_project
- page_title "Merge Requests"
- unless @project.default_issues_tracker?
@@ -10,6 +12,9 @@
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
+- if show_new_nav?
+ - content_for :breadcrumbs_extra do
+ = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path
= render 'projects/last_push'
@@ -17,13 +22,8 @@
%div{ class: container_class }
.top-area
= render 'shared/issuable/nav', type: :merge_requests
- .nav-controls
- - if @can_bulk_update
- = button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle"
- - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- - if merge_project
- = link_to namespace_project_new_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New merge request" do
- New merge request
+ .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path
= render 'shared/issuable/search_bar', type: :merge_requests
@@ -33,4 +33,4 @@
.merge-requests-holder
= render 'merge_requests'
- else
- = render 'shared/empty_states/merge_requests', button_path: namespace_project_new_merge_request_path(@project.namespace, @project)
+ = render 'shared/empty_states/merge_requests', button_path: new_merge_request_path
diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml
index 8ffddfe6154..78002e8cd64 100644
--- a/app/views/projects/pipelines/charts.html.haml
+++ b/app/views/projects/pipelines/charts.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- page_title "Charts", "Pipelines"
+- page_title _("Charts"), _("Pipelines")
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('graphs')
@@ -8,7 +8,7 @@
%div{ class: container_class }
.sub-header-block
.oneline
- A collection of graphs for Continuous Integration
+ = _("A collection of graphs regarding Continuous Integration")
#charts.ci-charts
.row
diff --git a/app/views/projects/pipelines/charts/_overall.haml b/app/views/projects/pipelines/charts/_overall.haml
index 93083397d5b..66786c7ff59 100644
--- a/app/views/projects/pipelines/charts/_overall.haml
+++ b/app/views/projects/pipelines/charts/_overall.haml
@@ -1,15 +1,15 @@
-%h4 Overall stats
+%h4= s_("PipelineCharts|Overall statistics")
%ul
%li
- Total:
- %strong= pluralize @counts[:total], 'pipeline'
+ = s_("PipelineCharts|Total:")
+ %strong= n_("1 pipeline", "%d pipelines", @counts[:total]) % @counts[:total]
%li
- Successful:
- %strong= pluralize @counts[:success], 'pipeline'
+ = s_("PipelineCharts|Successful:")
+ %strong= n_("1 pipeline", "%d pipelines", @counts[:success]) % @counts[:success]
%li
- Failed:
- %strong= pluralize @counts[:failed], 'pipeline'
+ = s_("PipelineCharts|Failed:")
+ %strong= n_("1 pipeline", "%d pipelines", @counts[:failed]) % @counts[:failed]
%li
- Success ratio:
+ = s_("PipelineCharts|Success ratio:")
%strong
#{success_ratio(@counts)}%
diff --git a/app/views/projects/pipelines/charts/_pipeline_times.haml b/app/views/projects/pipelines/charts/_pipeline_times.haml
index aee7c5492aa..1292f580a81 100644
--- a/app/views/projects/pipelines/charts/_pipeline_times.haml
+++ b/app/views/projects/pipelines/charts/_pipeline_times.haml
@@ -1,6 +1,6 @@
%div
%p.light
- Commit duration in minutes for last 30 commits
+ = _("Commit duration in minutes for last 30 commits")
%canvas#build_timesChart{ height: 200 }
diff --git a/app/views/projects/pipelines/charts/_pipelines.haml b/app/views/projects/pipelines/charts/_pipelines.haml
index b6f453b9736..be884448087 100644
--- a/app/views/projects/pipelines/charts/_pipelines.haml
+++ b/app/views/projects/pipelines/charts/_pipelines.haml
@@ -1,29 +1,29 @@
-%h4 Pipelines charts
+%h4= _("Pipelines charts")
%p
&nbsp;
%span.cgreen
= icon("circle")
- success
+ = s_("Pipeline|success")
&nbsp;
%span.cgray
= icon("circle")
- all
+ = s_("Pipeline|all")
.prepend-top-default
%p.light
- Jobs for last week
+ = _("Jobs for last week")
(#{date_from_to(Date.today - 7.days, Date.today)})
%canvas#weekChart{ height: 200 }
.prepend-top-default
%p.light
- Jobs for last month
+ = _("Jobs for last month")
(#{date_from_to(Date.today - 30.days, Date.today)})
%canvas#monthChart{ height: 200 }
.prepend-top-default
%p.light
- Jobs for last year
+ = _("Jobs for last year")
%canvas#yearChart.padded{ height: 250 }
- [:week, :month, :year].each do |scope|
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index e1e70a53709..152e50a79bb 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -73,7 +73,7 @@
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do
#{ _('Set up auto deploy') }
-%div{ class: container_class }
+%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- if @project.archived?
.text-warning.center.prepend-top-20
%p
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 3a49227961f..49555b6ff4e 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -1,6 +1,6 @@
- if @issues.to_a.any?
.panel.panel-default.panel-small.panel-without-border
- %ul.content-list.issues-list
+ %ul.content-list.issues-list.issuable-list
= render partial: 'projects/issues/issue', collection: @issues
= paginate @issues, theme: "gitlab"
- else
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index eecbb32e90e..0517896cfbd 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -1,6 +1,6 @@
- if @merge_requests.to_a.any?
.panel.panel-default.panel-small.panel-without-border
- %ul.content-list.mr-list
+ %ul.content-list.mr-list.issuable-list
= render partial: 'projects/merge_requests/merge_request', collection: @merge_requests
= paginate @merge_requests, theme: "gitlab"
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index a212c714826..785a500e44e 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -1,3 +1,5 @@
+- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
+
.dropdown.inline.prepend-left-10
%button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } }
- if @sort.present?
@@ -23,7 +25,7 @@
= sort_title_milestone_soon
= link_to page_filter_path(sort: sort_value_milestone_later, label: true) do
= sort_title_milestone_later
- - if controller.controller_name == 'issues' || controller.action_name == 'issues'
+ - if viewing_issues
= link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do
= sort_title_due_date_soon
= link_to page_filter_path(sort: sort_value_due_date_later, label: true) do
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index d3d290692a2..ae890567225 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -23,7 +23,7 @@
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
- %input.form-control.filtered-search{ id: "filtered-search-#{type.to_s}", placeholder: 'Search or filter results...', data: { 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } }
+ %input.form-control.filtered-search{ search_filter_input_options(type) }
= icon('filter')
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 2ea5eb960c0..57392cd7fbb 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -37,19 +37,20 @@
- issuable.assignees.each do |assignee|
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }
- - options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }
-
+ - options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: current_user&.username, current_user: true, project_id: @project&.id, author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }
- title = 'Select assignee'
- if issuable.is_a?(Issue)
- unless issuable.assignees.any?
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil
+ - dropdown_options = issue_assignees_dropdown_options
+ - title = dropdown_options[:title]
- options[:toggle_class] += ' js-multiselect js-save-user-data'
- data = { field_name: "#{issuable.to_ability_name}[assignee_ids][]" }
- data[:multi_select] = true
- data['dropdown-title'] = title
- - data['dropdown-header'] = 'Assignee'
- - data['max-select'] = 1
+ - data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
+ - data['max-select'] = dropdown_options[:data][:'max-select']
- options[:data].merge!(data)
= dropdown_tag(title, options: options)
diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml
index bfa91629e1e..8f6509a8ce8 100644
--- a/app/views/shared/issuable/form/_merge_params.html.haml
+++ b/app/views/shared/issuable/form/_merge_params.html.haml
@@ -11,8 +11,7 @@
.col-sm-10.col-sm-offset-2
- if issuable.can_remove_source_branch?(current_user)
.checkbox
- - initial_checkbox_value = issuable.merge_params.key?('force_remove_source_branch') ? issuable.force_remove_source_branch? : true
= label_tag 'merge_request[force_remove_source_branch]' do
= hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
- = check_box_tag 'merge_request[force_remove_source_branch]', '1', initial_checkbox_value
+ = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?
Remove source branch when merge request is accepted.
diff --git a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml
index 77175c839a6..567cde764e2 100644
--- a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml
+++ b/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml
@@ -7,5 +7,5 @@
- if issuable.assignees.length === 0
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' }
- = dropdown_tag(users_dropdown_label(issuable.assignees), options: issue_dropdown_options(issuable,false))
+ = dropdown_tag(users_dropdown_label(issuable.assignees), options: issue_assignees_dropdown_options)
= link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}"
diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml
index d97fdf179d7..40224cec9e8 100644
--- a/app/views/shared/members/_access_request_buttons.html.haml
+++ b/app/views/shared/members/_access_request_buttons.html.haml
@@ -1,18 +1,20 @@
- model_name = source.model_name.to_s.downcase
-.project-action-button.inline
- - if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id))
+- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id))
+ .project-action-button.inline
- link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project')
= link_to link_text, polymorphic_path([:leave, source, :members]),
method: :delete,
data: { confirm: leave_confirmation_message(source) },
class: 'btn'
- - elsif requester = source.requesters.find_by(user_id: current_user.id)
+- elsif requester = source.requesters.find_by(user_id: current_user.id)
+ .project-action-button.inline
= link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]),
method: :delete,
data: { confirm: remove_member_message(requester) },
class: 'btn'
- - elsif source.request_access_enabled && can?(current_user, :request_access, source)
+- elsif source.request_access_enabled && can?(current_user, :request_access, source)
+ .project-action-button.inline
= link_to _('Request Access'), polymorphic_path([:request_access, source, :members]),
method: :post,
class: 'btn'
diff --git a/changelogs/unreleased/23036-replace-dashboard-new-project-spinach.yml b/changelogs/unreleased/23036-replace-dashboard-new-project-spinach.yml
new file mode 100644
index 00000000000..a5f78202c93
--- /dev/null
+++ b/changelogs/unreleased/23036-replace-dashboard-new-project-spinach.yml
@@ -0,0 +1,4 @@
+---
+title: Replace 'dashboard/new-project.feature' spinach with rspec
+merge_request: 12550
+author: Alexander Randa (@randaalex)
diff --git a/changelogs/unreleased/32048-shared-runners-admin-buttons-have-odd-spacing.yml b/changelogs/unreleased/32048-shared-runners-admin-buttons-have-odd-spacing.yml
new file mode 100644
index 00000000000..99e64b9b467
--- /dev/null
+++ b/changelogs/unreleased/32048-shared-runners-admin-buttons-have-odd-spacing.yml
@@ -0,0 +1,4 @@
+---
+title: Fix spacing on runner buttons.
+merge_request: !12535
+author:
diff --git a/changelogs/unreleased/32885-unintentionally-removing-branch-when-merging-merge-request.yml b/changelogs/unreleased/32885-unintentionally-removing-branch-when-merging-merge-request.yml
new file mode 100644
index 00000000000..313aeab91b5
--- /dev/null
+++ b/changelogs/unreleased/32885-unintentionally-removing-branch-when-merging-merge-request.yml
@@ -0,0 +1,4 @@
+---
+title: Set default for Remove source branch to false.
+merge_request: !12576
+author:
diff --git a/changelogs/unreleased/33130-remove-group-modal.yml b/changelogs/unreleased/33130-remove-group-modal.yml
new file mode 100644
index 00000000000..4672d41ded5
--- /dev/null
+++ b/changelogs/unreleased/33130-remove-group-modal.yml
@@ -0,0 +1,4 @@
+---
+title: "Remove group modal like remove project modal (requires typing + confirmation)"
+merge_request: 12569
+author: Diego Souza
diff --git a/changelogs/unreleased/34097-issue-board-remove-from-board-button-when-viewing-an-issue-gives-js-error-and-fails.yml b/changelogs/unreleased/34097-issue-board-remove-from-board-button-when-viewing-an-issue-gives-js-error-and-fails.yml
new file mode 100644
index 00000000000..c0ea75adfa0
--- /dev/null
+++ b/changelogs/unreleased/34097-issue-board-remove-from-board-button-when-viewing-an-issue-gives-js-error-and-fails.yml
@@ -0,0 +1,4 @@
+---
+title: Remove "Remove from board" button from backlog and closed list
+merge_request: 12430
+author:
diff --git a/changelogs/unreleased/34116-milestone-filtering-on-group-issues.yml b/changelogs/unreleased/34116-milestone-filtering-on-group-issues.yml
new file mode 100644
index 00000000000..8f8b5a96c2b
--- /dev/null
+++ b/changelogs/unreleased/34116-milestone-filtering-on-group-issues.yml
@@ -0,0 +1,4 @@
+---
+title: Change milestone endpoint for groups
+merge_request: 12374
+author: Takuya Noguchi
diff --git a/changelogs/unreleased/34141-allow-unauthenticated-access-to-the-users-api.yml b/changelogs/unreleased/34141-allow-unauthenticated-access-to-the-users-api.yml
new file mode 100644
index 00000000000..a3ade8db214
--- /dev/null
+++ b/changelogs/unreleased/34141-allow-unauthenticated-access-to-the-users-api.yml
@@ -0,0 +1,4 @@
+---
+title: Allow unauthenticated access to the /api/v4/users API
+merge_request: 12445
+author:
diff --git a/changelogs/unreleased/adam-external-issue-references-spike.yml b/changelogs/unreleased/adam-external-issue-references-spike.yml
new file mode 100644
index 00000000000..aeec6688425
--- /dev/null
+++ b/changelogs/unreleased/adam-external-issue-references-spike.yml
@@ -0,0 +1,4 @@
+---
+title: Improve support for external issue references
+merge_request: 12485
+author:
diff --git a/changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml b/changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml
new file mode 100644
index 00000000000..4948d415bed
--- /dev/null
+++ b/changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml
@@ -0,0 +1,6 @@
+---
+title: Rename duplicated variables with the same key for projects. Add environment_scope
+ column to variables and add unique constraint to make sure that no variables could
+ be created with the same key within a project
+merge_request: 12363
+author:
diff --git a/changelogs/unreleased/dm-dependency-linker-newlines.yml b/changelogs/unreleased/dm-dependency-linker-newlines.yml
deleted file mode 100644
index 5631095fcb7..00000000000
--- a/changelogs/unreleased/dm-dependency-linker-newlines.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix diff of requirements.txt file by not matching newlines as part of package
- names
-merge_request:
-author:
diff --git a/changelogs/unreleased/dm-empty-state-new-merge-request.yml b/changelogs/unreleased/dm-empty-state-new-merge-request.yml
new file mode 100644
index 00000000000..5fad7a0f883
--- /dev/null
+++ b/changelogs/unreleased/dm-empty-state-new-merge-request.yml
@@ -0,0 +1,5 @@
+---
+title: Fix 'New merge request' button for users who don't have push access to canonical
+ project
+merge_request:
+author:
diff --git a/changelogs/unreleased/enable-webpack-code-splitting.yml b/changelogs/unreleased/enable-webpack-code-splitting.yml
new file mode 100644
index 00000000000..d61c3b97d11
--- /dev/null
+++ b/changelogs/unreleased/enable-webpack-code-splitting.yml
@@ -0,0 +1,5 @@
+---
+title: Enable support for webpack code-splitting by dynamically setting publicPath
+ at runtime
+merge_request: 12032
+author:
diff --git a/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml b/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml
new file mode 100644
index 00000000000..bbcf2946ea7
--- /dev/null
+++ b/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml
@@ -0,0 +1,4 @@
+---
+title: Omit trailing / leading hyphens in CI_COMMIT_REF_SLUG variable to make it usable as a hostname
+merge_request: 11218
+author: Stefan Hanreich
diff --git a/changelogs/unreleased/fix-2801.yml b/changelogs/unreleased/fix-2801.yml
new file mode 100644
index 00000000000..4f0aa06b6ba
--- /dev/null
+++ b/changelogs/unreleased/fix-2801.yml
@@ -0,0 +1,4 @@
+---
+title: Expires full_path cache after a repository is renamed/transferred
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-34417.yml b/changelogs/unreleased/fix-34417.yml
deleted file mode 100644
index 5f012ad0c81..00000000000
--- a/changelogs/unreleased/fix-34417.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Perform housekeeping only when an import of a fresh project is completed
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-assigned-issuable-lists.yml b/changelogs/unreleased/fix-assigned-issuable-lists.yml
new file mode 100644
index 00000000000..fc2cd18ddb6
--- /dev/null
+++ b/changelogs/unreleased/fix-assigned-issuable-lists.yml
@@ -0,0 +1,5 @@
+---
+title: Add issuable-list class to shared mr/issue lists to fix new responsive layout
+ design
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-head-pipeline-for-commit-status.yml b/changelogs/unreleased/fix-head-pipeline-for-commit-status.yml
deleted file mode 100644
index f12e7b53790..00000000000
--- a/changelogs/unreleased/fix-head-pipeline-for-commit-status.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix head pipeline stored in merge request for external pipelines
-merge_request: 12478
-author:
diff --git a/changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml b/changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml
new file mode 100644
index 00000000000..856990a6126
--- /dev/null
+++ b/changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed sidebar not collapsing on merge requests in mobile screens
+merge_request:
+author:
diff --git a/changelogs/unreleased/hb-hide-archived-labels-from-group-issue-tracker.yml b/changelogs/unreleased/hb-hide-archived-labels-from-group-issue-tracker.yml
new file mode 100644
index 00000000000..3b465d84126
--- /dev/null
+++ b/changelogs/unreleased/hb-hide-archived-labels-from-group-issue-tracker.yml
@@ -0,0 +1,4 @@
+---
+title: Hide archived project labels from group issue tracker
+merge_request: 12547
+author: Horacio Bertorello
diff --git a/changelogs/unreleased/highest-return-on-diff-investment.yml b/changelogs/unreleased/highest-return-on-diff-investment.yml
deleted file mode 100644
index c8be1e0ff8f..00000000000
--- a/changelogs/unreleased/highest-return-on-diff-investment.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Bring back branches badge to main project page
-merge_request: 12548
-author:
diff --git a/changelogs/unreleased/issue-boards-closed-list-all.yml b/changelogs/unreleased/issue-boards-closed-list-all.yml
deleted file mode 100644
index 7643864150d..00000000000
--- a/changelogs/unreleased/issue-boards-closed-list-all.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed issue boards closed list not showing all closed issues
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue-form-multiple-line-markdown.yml b/changelogs/unreleased/issue-form-multiple-line-markdown.yml
deleted file mode 100644
index 23128f346bc..00000000000
--- a/changelogs/unreleased/issue-form-multiple-line-markdown.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed multi-line markdown tooltip buttons in issue edit form
-merge_request:
-author:
diff --git a/changelogs/unreleased/monitoring-dashboard-fix-y-label.yml b/changelogs/unreleased/monitoring-dashboard-fix-y-label.yml
new file mode 100644
index 00000000000..8a0e9ca855c
--- /dev/null
+++ b/changelogs/unreleased/monitoring-dashboard-fix-y-label.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed the y_label not setting correctly for each graph on the monitoring dashboard
+merge_request:
+author:
diff --git a/changelogs/unreleased/sh-allow-force-repo-create.yml b/changelogs/unreleased/sh-allow-force-repo-create.yml
new file mode 100644
index 00000000000..2a65ba807bb
--- /dev/null
+++ b/changelogs/unreleased/sh-allow-force-repo-create.yml
@@ -0,0 +1,4 @@
+---
+title: Make Project#ensure_repository force create a repo
+merge_request:
+author:
diff --git a/changelogs/unreleased/sh-optimize-project-commit-api.yml b/changelogs/unreleased/sh-optimize-project-commit-api.yml
new file mode 100644
index 00000000000..e6a8a80593c
--- /dev/null
+++ b/changelogs/unreleased/sh-optimize-project-commit-api.yml
@@ -0,0 +1,4 @@
+---
+title: Optimize creation of commit API by using Repository#commit instead of Repository#commits
+merge_request:
+author:
diff --git a/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml b/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml
new file mode 100644
index 00000000000..6bf03d9a382
--- /dev/null
+++ b/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml
@@ -0,0 +1,5 @@
+---
+title: Cache open issue and merge request counts for project tabs to speed up project
+ pages
+merge_request: 12457
+author:
diff --git a/changelogs/unreleased/tc-no-delete-protected-merged-branches.yml b/changelogs/unreleased/tc-no-delete-protected-merged-branches.yml
new file mode 100644
index 00000000000..663a0349bac
--- /dev/null
+++ b/changelogs/unreleased/tc-no-delete-protected-merged-branches.yml
@@ -0,0 +1,4 @@
+---
+title: Do not delete protected branches when deleting all merged branches
+merge_request: 12624
+author:
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 43a8c0078ca..4b81fd90f59 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -543,6 +543,10 @@ production: &base
# enabled: true
# host: localhost
# port: 3808
+ prometheus:
+ # Time between sampling of unicorn socket metrics, in seconds
+ # unicorn_sampler_interval: 10
+
#
# 5. Extra customization
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 8ddf8e4d2e4..cb11d2c34f4 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -495,6 +495,12 @@ Settings.webpack.dev_server['host'] ||= 'localhost'
Settings.webpack.dev_server['port'] ||= 3808
#
+# Prometheus metrics settings
+#
+Settings['prometheus'] ||= Settingslogic.new({})
+Settings.prometheus['unicorn_sampler_interval'] ||= 10
+
+#
# Testing settings
#
if Rails.env.test?
diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb
index a0a63ddf8f0..d56fd7a6cfa 100644
--- a/config/initializers/8_metrics.rb
+++ b/config/initializers/8_metrics.rb
@@ -119,6 +119,13 @@ def instrument_classes(instrumentation)
end
# rubocop:enable Metrics/AbcSize
+Gitlab::Metrics::UnicornSampler.initialize_instance(Settings.prometheus.unicorn_sampler_interval).start
+
+Gitlab::Application.configure do |config|
+ # 0 should be Sentry to catch errors in this middleware
+ config.middleware.insert(1, Gitlab::Metrics::ConnectionRackMiddleware)
+end
+
if Gitlab::Metrics.enabled?
require 'pathname'
require 'influxdb'
@@ -175,7 +182,7 @@ if Gitlab::Metrics.enabled?
GC::Profiler.enable
- Gitlab::Metrics::Sampler.new.start
+ Gitlab::Metrics::InfluxSampler.initialize_instance.start
module TrackNewRedisConnections
def connect(*args)
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 90ef6a5448b..cbb0a899638 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -71,6 +71,7 @@ var config = {
vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js',
peek: './peek.js',
+ webpack_runtime: './webpack.js',
},
output: {
@@ -190,7 +191,7 @@ var config = {
// create cacheable common library bundles
new webpack.optimize.CommonsChunkPlugin({
- names: ['main', 'locale', 'common', 'runtime'],
+ names: ['main', 'locale', 'common', 'webpack_runtime'],
}),
],
@@ -245,7 +246,6 @@ if (IS_DEV_SERVER) {
hot: DEV_SERVER_LIVERELOAD,
inline: DEV_SERVER_LIVERELOAD
};
- config.output.publicPath = '//' + DEV_SERVER_HOST + ':' + DEV_SERVER_PORT + config.output.publicPath;
config.plugins.push(
// watch node_modules for changes if we encounter a missing module compile error
new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules'))
diff --git a/db/migrate/20170622135451_rename_duplicated_variable_key.rb b/db/migrate/20170622135451_rename_duplicated_variable_key.rb
new file mode 100644
index 00000000000..368718ab0ce
--- /dev/null
+++ b/db/migrate/20170622135451_rename_duplicated_variable_key.rb
@@ -0,0 +1,38 @@
+class RenameDuplicatedVariableKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ execute(<<~SQL)
+ UPDATE ci_variables
+ SET #{key} = CONCAT(#{key}, #{underscore}, id)
+ WHERE id IN (
+ SELECT *
+ FROM ( -- MySQL requires an extra layer
+ SELECT dup.id
+ FROM ci_variables dup
+ INNER JOIN (SELECT max(id) AS id, #{key}, project_id
+ FROM ci_variables tmp
+ GROUP BY #{key}, project_id) var
+ USING (#{key}, project_id) where dup.id <> var.id
+ ) dummy
+ )
+ SQL
+ end
+
+ def down
+ # noop
+ end
+
+ def key
+ # key needs to be quoted in MySQL
+ quote_column_name('key')
+ end
+
+ def underscore
+ quote('_')
+ end
+end
diff --git a/db/migrate/20170622135628_add_environment_scope_to_ci_variables.rb b/db/migrate/20170622135628_add_environment_scope_to_ci_variables.rb
new file mode 100644
index 00000000000..17fe062d8d5
--- /dev/null
+++ b/db/migrate/20170622135628_add_environment_scope_to_ci_variables.rb
@@ -0,0 +1,15 @@
+class AddEnvironmentScopeToCiVariables < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:ci_variables, :environment_scope, :string, default: '*')
+ end
+
+ def down
+ remove_column(:ci_variables, :environment_scope)
+ end
+end
diff --git a/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb
new file mode 100644
index 00000000000..8b2cc40ee59
--- /dev/null
+++ b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb
@@ -0,0 +1,38 @@
+class AddUniqueConstraintToCiVariables < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ unless this_index_exists?
+ add_concurrent_index(:ci_variables, columns, name: index_name, unique: true)
+ end
+ end
+
+ def down
+ if this_index_exists?
+ if Gitlab::Database.mysql? && !index_exists?(:ci_variables, :project_id)
+ # Need to add this index for MySQL project_id foreign key constraint
+ add_concurrent_index(:ci_variables, :project_id)
+ end
+
+ remove_concurrent_index(:ci_variables, columns, name: index_name)
+ end
+ end
+
+ private
+
+ def this_index_exists?
+ index_exists?(:ci_variables, columns, name: index_name)
+ end
+
+ def columns
+ @columns ||= [:project_id, :key, :environment_scope]
+ end
+
+ def index_name
+ 'index_ci_variables_on_project_id_and_key_and_environment_scope'
+ end
+end
diff --git a/db/migrate/20170623080805_remove_ci_variables_project_id_index.rb b/db/migrate/20170623080805_remove_ci_variables_project_id_index.rb
new file mode 100644
index 00000000000..ddcc0292b9d
--- /dev/null
+++ b/db/migrate/20170623080805_remove_ci_variables_project_id_index.rb
@@ -0,0 +1,19 @@
+class RemoveCiVariablesProjectIdIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ if index_exists?(:ci_variables, :project_id)
+ remove_concurrent_index(:ci_variables, :project_id)
+ end
+ end
+
+ def down
+ unless index_exists?(:ci_variables, :project_id)
+ add_concurrent_index(:ci_variables, :project_id)
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 8c7440ee610..993eea1f642 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: 20170622162730) do
+ActiveRecord::Schema.define(version: 20170623080805) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -374,9 +374,10 @@ ActiveRecord::Schema.define(version: 20170622162730) do
t.string "encrypted_value_iv"
t.integer "project_id", null: false
t.boolean "protected", default: false, null: false
+ t.string "environment_scope", default: "*", null: false
end
- add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
+ add_index "ci_variables", ["project_id", "key", "environment_scope"], name: "index_ci_variables_on_project_id_and_key_and_environment_scope", unique: true, using: :btree
create_table "container_repositories", force: :cascade do |t|
t.integer "project_id", null: false
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index 18ceb8f779e..1fc577561a0 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -61,7 +61,7 @@ POST /projects/:id/repository/files/:file_path
```
```bash
-curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fprojectrb%2E?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file'
+curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fprojectrb%2E?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file'
```
Example response:
@@ -90,7 +90,7 @@ PUT /projects/:id/repository/files/:file_path
```
```bash
-curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file'
+curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file'
```
Example response:
@@ -129,7 +129,7 @@ DELETE /projects/:id/repository/files/:file_path
```
```bash
-curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
+curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
```
Example response:
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index be4dea55c20..d3433594eb7 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -1,4 +1,4 @@
-# Using Docker Images
+# Using Docker images
GitLab CI in conjunction with [GitLab Runner](../runners/README.md) can use
[Docker Engine](https://www.docker.com/) to test and build any application.
@@ -17,14 +17,16 @@ can also run on your workstation. The added benefit is that you can test all
the commands that we will explore later from your shell, rather than having to
test them on a dedicated CI server.
-## Register docker runner
+## Register Docker Runner
-To use GitLab Runner with docker you need to register a new runner to use the
-`docker` executor:
+To use GitLab Runner with Docker you need to [register a new Runner][register]
+to use the `docker` executor.
+
+A one-line example can be seen below:
```bash
-gitlab-ci-multi-runner register \
- --url "https://gitlab.com/" \
+sudo gitlab-runner register \
+ --url "https://gitlab.example.com/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
--description "docker-ruby-2.1" \
--executor "docker" \
@@ -33,26 +35,26 @@ gitlab-ci-multi-runner register \
--docker-mysql latest
```
-The registered runner will use the `ruby:2.1` docker image and will run two
+The registered runner will use the `ruby:2.1` Docker image and will run two
services, `postgres:latest` and `mysql:latest`, both of which will be
accessible during the build process.
## What is an image
-The `image` keyword is the name of the docker image the docker executor
-will run to perform the CI tasks.
+The `image` keyword is the name of the Docker image the Docker executor
+will run to perform the CI tasks.
-By default the executor will only pull images from [Docker Hub][hub],
+By default, the executor will only pull images from [Docker Hub][hub],
but this can be configured in the `gitlab-runner/config.toml` by setting
-the [docker pull policy][] to allow using local images.
+the [Docker pull policy][] to allow using local images.
For more information about images and Docker Hub please read
the [Docker Fundamentals][] documentation.
## What is a service
-The `services` keyword defines just another docker image that is run during
-your job and is linked to the docker image that the `image` keyword defines.
+The `services` keyword defines just another Docker image that is run during
+your job and is linked to the Docker image that the `image` keyword defines.
This allows you to access the service image during build time.
The service image can run any application, but the most common use case is to
@@ -60,6 +62,11 @@ run a database container, eg. `mysql`. It's easier and faster to use an
existing image and run it as an additional container than install `mysql` every
time the project is built.
+You are not limited to have only database services. You can add as many
+services you need to `.gitlab-ci.yml` or manually modify `config.toml`.
+Any image found at [Docker Hub][hub] or your private Container Registry can be
+used as a service.
+
You can see some widely used services examples in the relevant documentation of
[CI services examples](../services/README.md).
@@ -73,22 +80,49 @@ then be used to create a container that is linked to the job container.
The service container for MySQL will be accessible under the hostname `mysql`.
So, in order to access your database service you have to connect to the host
-named `mysql` instead of a socket or `localhost`.
+named `mysql` instead of a socket or `localhost`. Read more in [accessing the
+services](#accessing-the-services).
-## Overwrite image and services
+### Accessing the services
-See [How to use other images as services](#how-to-use-other-images-as-services).
+Let's say that you need a Wordpress instance to test some API integration with
+your application.
-## How to use other images as services
+You can then use for example the [tutum/wordpress][] image in your
+`.gitlab-ci.yml`:
-You are not limited to have only database services. You can add as many
-services you need to `.gitlab-ci.yml` or manually modify `config.toml`.
-Any image found at [Docker Hub][hub] can be used as a service.
+```yaml
+services:
+- tutum/wordpress:latest
+```
+
+If you don't [specify a service alias](#available-settings-for-services-entry),
+when the job is run, `tutum/wordpress` will be started and you will have
+access to it from your build container under two hostnames to choose from:
-## Define image and services from `.gitlab-ci.yml`
+- `tutum-wordpress`
+- `tutum__wordpress`
+
+>**Note:**
+Hostnames with underscores are not RFC valid and may cause problems in 3rd party
+applications.
+
+The default aliases for the service's hostname are created from its image name
+following these rules:
+
+- Everything after the colon (`:`) is stripped
+- Slash (`/`) is replaced with double underscores (`__`) and the primary alias
+ is created
+- Slash (`/`) is replaced with a single dash (`-`) and the secondary alias is
+ created (requires GitLab Runner v1.1.0 or higher)
+
+To override the default behavior, you can
+[specify a service alias](#available-settings-for-services-entry).
+
+## Define `image` and `services` from `.gitlab-ci.yml`
You can simply define an image that will be used for all jobs and a list of
-services that you want to use during build time.
+services that you want to use during build time:
```yaml
image: ruby:2.2
@@ -125,6 +159,203 @@ test:2.2:
- bundle exec rake spec
```
+Or you can pass some [extended configuration options](#extended-docker-configuration-options)
+for `image` and `services`:
+
+```yaml
+image:
+ name: ruby:2.2
+ entrypoint: ["/bin/bash"]
+
+services:
+- name: my-postgres:9.4
+ alias: db-postgres
+ entrypoint: ["/usr/local/bin/db-postgres"]
+ command: ["start"]
+
+before_script:
+- bundle install
+
+test:
+ script:
+ - bundle exec rake spec
+```
+
+## Extended Docker configuration options
+
+> **Note:**
+This feature requires GitLab 9.4 and GitLab Runner 9.4 or higher.
+
+When configuring the `image` or `services` entries, you can use a string or a map as
+options:
+
+- when using a string as an option, it must be the full name of the image to use
+ (including the Registry part if you want to download the image from a Registry
+ other than Docker Hub)
+- when using a map as an option, then it must contain at least the `name`
+ option, which is the same name of the image as used for the string setting
+
+For example, the following two definitions are equal:
+
+1. Using a string as an option to `image` and `services`:
+
+ ```yaml
+ image: "registry.example.com/my/image:latest"
+
+ services:
+ - postgresql:9.4
+ - redis:latest
+ ```
+
+1. Using a map as an option to `image` and `services`. The use of `image:name` is
+ required:
+
+ ```yaml
+ image:
+ name: "registry.example.com/my/image:latest"
+
+ services:
+ - name: postgresql:9.4
+ - name: redis:latest
+ ```
+
+### Available settings for `image`
+
+> **Note:**
+This feature requires GitLab 9.4 and GitLab Runner 9.4 or higher.
+
+| Setting | Required | Description |
+|------------|----------|-------------|
+| `name` | yes, when used with any other option | Full name of the image that should be used. It should contain the Registry part if needed. |
+| `entrypoint` | no | Command or script that should be executed as the container's entrypoint. It will be translated to Docker's `--entrypoint` option while creating the container. The syntax is similar to [`Dockerfile`'s `ENTRYPOINT`][entrypoint] directive, where each shell token is a separate string in the array. |
+
+### Available settings for `services`
+
+> **Note:**
+This feature requires GitLab 9.4 and GitLab Runner 9.4 or higher.
+
+| Setting | Required | Description |
+|------------|----------|-------------|
+| `name` | yes, when used with any other option | Full name of the image that should be used. It should contain the Registry part if needed. |
+| `entrypoint` | no | Command or script that should be executed as the container's entrypoint. It will be translated to Docker's `--entrypoint` option while creating the container. The syntax is similar to [`Dockerfile`'s `ENTRYPOINT`][entrypoint] directive, where each shell token is a separate string in the array. |
+| `command` | no | Command or script that should be used as the container's command. It will be translated to arguments passed to Docker after the image's name. The syntax is similar to [`Dockerfile`'s `CMD`][cmd] directive, where each shell token is a separate string in the array. |
+| `alias` | no | Additional alias that can be used to access the service from the job's container. Read [Accessing the services](#accessing-the-services) for more information. |
+
+### Starting multiple services from the same image
+
+Before the new extended Docker configuration options, the following configuration
+would not work properly:
+
+```yaml
+services:
+- mysql:latest
+- mysql:latest
+```
+
+The Runner would start two containers using the `mysql:latest` image, but both
+of them would be added to the job's container with the `mysql` alias based on
+the [default hostname naming](#accessing-the-services). This would end with one
+of the services not being accessible.
+
+After the new extended Docker configuration options, the above example would
+look like:
+
+```yaml
+services:
+- name: mysql:latest
+ alias: mysql-1
+- name: mysql:latest
+ alias: mysql-2
+```
+
+The Runner will still start two containers using the `mysql:latest` image,
+but now each of them will also be accessible with the alias configured
+in `.gitlab-ci.yml` file.
+
+### Setting a command for the service
+
+Let's assume you have a `super/sql:latest` image with some SQL database
+inside it and you would like to use it as a service for your job. Let's also
+assume that this image doesn't start the database process while starting
+the container and the user needs to manually use `/usr/bin/super-sql run` as
+a command to start the database.
+
+Before the new extended Docker configuration options, you would need to create
+your own image based on the `super/sql:latest` image, add the default command,
+and then use it in job's configuration, like:
+
+```Dockerfile
+# my-super-sql:latest image's Dockerfile
+
+FROM super/sql:latest
+CMD ["/usr/bin/super-sql", "run"]
+```
+
+```yaml
+# .gitlab-ci.yml
+
+services:
+- my-super-sql:latest
+```
+
+After the new extended Docker configuration options, you can now simply
+set a `command` in `.gitlab-ci.yml`, like:
+
+```yaml
+# .gitlab-ci.yml
+
+services:
+- name: super/sql:latest
+ command: ["/usr/bin/super-sql", "run"]
+```
+
+As you can see, the syntax of `command` is similar to [Dockerfile's `CMD`][cmd].
+
+### Overriding the entrypoint of an image
+
+Let's assume you have a `super/sql:experimental` image with some SQL database
+inside it and you would like to use it as a base image for your job because you
+want to execute some tests with this database binary. Let's also assume that
+this image is configured with `/usr/bin/super-sql run` as an entrypoint. That
+means, that when starting the container without additional options, it will run
+the database's process, while Runner expects that the image will have no
+entrypoint or at least will start with a shell as its entrypoint.
+
+Previously we would need to create our own image based on the
+`super/sql:experimental` image, set the entrypoint to a shell, and then use
+it in job's configuration, e.g.:
+
+Before the new extended Docker configuration options, you would need to create
+your own image based on the `super/sql:experimental` image, set the entrypoint
+to a shell and then use it in job's configuration, like:
+
+```Dockerfile
+# my-super-sql:experimental image's Dockerfile
+
+FROM super/sql:experimental
+ENTRYPOINT ["/bin/sh"]
+```
+
+```yaml
+# .gitlab-ci.yml
+
+image: my-super-sql:experimental
+```
+
+After the new extended Docker configuration options, you can now simply
+set an `entrypoint` in `.gitlab-ci.yml`, like:
+
+```yaml
+# .gitlab-ci.yml
+
+image:
+ name: super/sql:experimental
+ entrypoint: ["/bin/sh"]
+```
+
+As you can see the syntax of `entrypoint` is similar to
+[Dockerfile's `ENTRYPOINT`][entrypoint].
+
## Define image and services in `config.toml`
Look for the `[runners.docker]` section:
@@ -138,7 +369,7 @@ Look for the `[runners.docker]` section:
The image and services defined this way will be added to all job run by
that runner.
-## Define an image from a private Docker registry
+## Define an image from a private Container Registry
> **Notes:**
- This feature requires GitLab Runner **1.8** or higher
@@ -193,44 +424,6 @@ To configure access for `registry.example.com`, follow these steps:
You can add configuration for as many registries as you want, adding more
registries to the `"auths"` hash as described above.
-## Accessing the services
-
-Let's say that you need a Wordpress instance to test some API integration with
-your application.
-
-You can then use for example the [tutum/wordpress][] image in your
-`.gitlab-ci.yml`:
-
-```yaml
-services:
-- tutum/wordpress:latest
-```
-
-When the job is run, `tutum/wordpress` will be started and you will have
-access to it from your build container under the hostnames `tutum-wordpress`
-(requires GitLab Runner v1.1.0 or newer) and `tutum__wordpress`.
-
-When using a private registry, the image name also includes a hostname and port
-of the registry.
-
-```yaml
-services:
-- docker.example.com:5000/wordpress:latest
-```
-
-The service hostname will also include the registry hostname. Service will be
-available under hostnames `docker.example.com-wordpress` (requires GitLab Runner v1.1.0 or newer)
-and `docker.example.com__wordpress`.
-
-*Note: hostname with underscores is not RFC valid and may cause problems in 3rd party applications.*
-
-The alias hostnames for the service are made from the image name following these
-rules:
-
-1. Everything after `:` is stripped
-2. Slash (`/`) is replaced with double underscores (`__`) - primary alias
-3. Slash (`/`) is replaced with dash (`-`) - secondary alias, requires GitLab Runner v1.1.0 or newer
-
## Configuring services
Many services accept environment variables which allow you to easily change
@@ -257,7 +450,7 @@ See the specific documentation for
## How Docker integration works
-Below is a high level overview of the steps performed by docker during job
+Below is a high level overview of the steps performed by Docker during job
time.
1. Create any service container: `mysql`, `postgresql`, `mongodb`, `redis`.
@@ -274,7 +467,7 @@ time.
## How to debug a job locally
*Note: The following commands are run without root privileges. You should be
-able to run docker with your regular user account.*
+able to run Docker with your regular user account.*
First start with creating a file named `build_script`:
@@ -334,3 +527,6 @@ creation.
[mysql-hub]: https://hub.docker.com/r/_/mysql/
[runner-priv-reg]: http://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry
[secret variable]: ../variables/README.md#secret-variables
+[entrypoint]: https://docs.docker.com/engine/reference/builder/#entrypoint
+[cmd]: https://docs.docker.com/engine/reference/builder/#cmd
+[register]: https://docs.gitlab.com/runner/register/
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index d1f9881e51b..eef96f3194f 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -37,7 +37,7 @@ future GitLab releases.**
|-------------------------------- |--------|--------|-------------|
| **CI** | all | 0.4 | Mark that job is executed in CI environment |
| **CI_COMMIT_REF_NAME** | 9.0 | all | The branch or tag name for which project is built |
-| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
+| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. |
| **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built |
| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md
index 265c891cf83..2dd9b33273c 100644
--- a/doc/integration/external-issue-tracker.md
+++ b/doc/integration/external-issue-tracker.md
@@ -8,6 +8,9 @@ you to do the following:
issue index of the external tracker
- clicking **New issue** on the project dashboard creates a new issue on the
external tracker
+- you can reference these external issues inside GitLab interface
+ (merge requests, commits, comments) and they will be automatically converted
+ into links
## Configuration
diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md
index e7d97fde14e..225a4dcc924 100644
--- a/doc/update/9.1-to-9.2.md
+++ b/doc/update/9.1-to-9.2.md
@@ -70,7 +70,27 @@ curl --location https://yarnpkg.com/install.sh | bash -
More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
-### 5. Get latest code
+### 5. Update Go
+
+NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go
+1.5.x through 1.7.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz
+echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.8.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
```bash
cd /home/git/gitlab
@@ -97,7 +117,7 @@ cd /home/git/gitlab
sudo -u git -H git checkout 9-2-stable-ee
```
-### 6. Update gitlab-shell
+### 7. Update gitlab-shell
```bash
cd /home/git/gitlab-shell
@@ -107,11 +127,10 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
sudo -u git -H bin/compile
```
-### 7. Update gitlab-workhorse
+### 8. Update gitlab-workhorse
-Install and compile gitlab-workhorse. This requires
-[Go 1.8](https://golang.org/dl). Go (at least 1.5) should already be on your system from
-GitLab 8.1 and shall be upgraded if necessary. Please note that starting in Gitlab 9.3, only Go 1.8.3 and above will be supported. GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/).
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
If you are not using Linux you may have to run `gmake` instead of
`make` below.
@@ -123,7 +142,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
sudo -u git -H make
```
-### 8. Update configuration files
+### 9. Update configuration files
#### New configuration options for `gitlab.yml`
@@ -197,7 +216,7 @@ For Ubuntu 16.04.1 LTS:
sudo systemctl daemon-reload
```
-### 9. Install libs, migrations, etc.
+### 10. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
@@ -223,7 +242,7 @@ sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
-### 10. Optional: install Gitaly
+### 11. Optional: install Gitaly
Gitaly is still an optional component of GitLab. If you want to save time
during your 9.2 upgrade **you can skip this step**.
@@ -240,14 +259,14 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
sudo -u git -H make
```
-### 11. Start application
+### 12. Start application
```bash
sudo service gitlab start
sudo service nginx restart
```
-### 12. Check application status
+### 13. Check application status
Check if GitLab and its environment are configured correctly:
diff --git a/doc/update/9.2-to-9.3.md b/doc/update/9.2-to-9.3.md
index 3e3dda722b6..097b996ec31 100644
--- a/doc/update/9.2-to-9.3.md
+++ b/doc/update/9.2-to-9.3.md
@@ -72,8 +72,8 @@ More information can be found on the [yarn website](https://yarnpkg.com/en/docs/
### 5. Update Go
-NOTE: GitLab 9.3 and higher only supports Go 1.8.3 and dropped support for Go 1.5.x through 1.7.x. Be
-sure to upgrade your installation if necessary
+NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go
+1.5.x through 1.7.x. Be sure to upgrade your installation if necessary.
You can check which version you are running with `go version`.
@@ -129,9 +129,8 @@ sudo -u git -H bin/compile
### 8. Update gitlab-workhorse
-Install and compile gitlab-workhorse. This requires
-[Go 1.5](https://golang.org/dl) which should already be on your system from
-GitLab 8.1. GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/).
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
If you are not using Linux you may have to run `gmake` instead of
`make` below.
diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png
index b636cb294b8..cf7f519f783 100644
--- a/doc/user/project/img/issue_board.png
+++ b/doc/user/project/img/issue_board.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_add_list.png b/doc/user/project/img/issue_board_add_list.png
index cdfc466d23f..973d9f7cde4 100644
--- a/doc/user/project/img/issue_board_add_list.png
+++ b/doc/user/project/img/issue_board_add_list.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_move_issue_card_list.png b/doc/user/project/img/issue_board_move_issue_card_list.png
new file mode 100644
index 00000000000..c6b17ada40e
--- /dev/null
+++ b/doc/user/project/img/issue_board_move_issue_card_list.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_welcome_message.png b/doc/user/project/img/issue_board_welcome_message.png
index 5318e6ea4a9..127b9b08cc7 100644
--- a/doc/user/project/img/issue_board_welcome_message.png
+++ b/doc/user/project/img/issue_board_welcome_message.png
Binary files differ
diff --git a/doc/user/project/img/issue_boards_add_issues_modal.png b/doc/user/project/img/issue_boards_add_issues_modal.png
index 33049dce74f..bedaf724a15 100644
--- a/doc/user/project/img/issue_boards_add_issues_modal.png
+++ b/doc/user/project/img/issue_boards_add_issues_modal.png
Binary files differ
diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md
index 0b219e84478..6a040516231 100644
--- a/doc/user/project/integrations/bugzilla.md
+++ b/doc/user/project/integrations/bugzilla.md
@@ -16,3 +16,14 @@ Once you have configured and enabled Bugzilla:
- the **Issues** link on the GitLab project pages takes you to the appropriate
Bugzilla product page
- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue
+
+## Referencing issues in Bugzilla
+
+Issues in Bugzilla can be referenced in two alternative ways:
+1. `#<ID>` where `<ID>` is a number (example `#143`)
+2. `<PROJECT>-<ID>` where `<PROJECT>` starts with a capital letter which is
+ then followed by capital letters, numbers or underscores, and `<ID>` is
+ a number (example `API_32-143`).
+
+Please note that `<PROJECT>` part is ignored and links always point to the
+address specified in `issues_url`.
diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md
index 89c0312d3c2..8026f1f57bc 100644
--- a/doc/user/project/integrations/redmine.md
+++ b/doc/user/project/integrations/redmine.md
@@ -21,3 +21,14 @@ Once you have configured and enabled Redmine:
As an example, below is a configuration for a project named gitlab-ci.
![Redmine configuration](img/redmine_configuration.png)
+
+## Referencing issues in Redmine
+
+Issues in Redmine can be referenced in two alternative ways:
+1. `#<ID>` where `<ID>` is a number (example `#143`)
+2. `<PROJECT>-<ID>` where `<PROJECT>` starts with a capital letter which is
+ then followed by capital letters, numbers or underscores, and `<ID>` is
+ a number (example `API_32-143`).
+
+Please note that `<PROJECT>` part is ignored and links always point to the
+address specified in `issues_url`.
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index ebea7062ecb..e2cc67726e0 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -1,8 +1,7 @@
-# Issue board
+# Issue Board
->**Notes:**
-- [Introduced][ce-5554] in GitLab 8.11.
-- The Backlog column was replaced by the **Add issues** button in GitLab 8.17.
+>**Note:**
+[Introduced][ce-5554] in [GitLab 8.11](https://about.gitlab.com/2016/08/22/gitlab-8-11-released/#issue-board).
The GitLab Issue Board is a software project management tool used to plan,
organize, and visualize a workflow for a feature or product release.
@@ -15,12 +14,65 @@ Other interesting links:
## Overview
-The Issue Board builds on GitLab's existing issue tracking functionality and
+The Issue Board builds on GitLab's existing
+[issue tracking functionality](issues/index.md#issue-tracker) and
leverages the power of [labels] by utilizing them as lists of the scrum board.
-With the Issue Board you can have a different view of your issues while also
+With the Issue Board you can have a different view of your issues while
maintaining the same filtering and sorting abilities you see across the
-issue tracker.
+issue tracker. An Issue Board is based on its project's label structure, therefore, it
+applies the same descriptive labels to indicate placement on the board, keeping
+consistency throughout the entire development lifecycle.
+
+An Issue Board shows you what issues your team is working on, who is assigned to each,
+and where in the workflow those issues are.
+
+You create issues, host code, perform reviews, build, test,
+and deploy from one single platform. Issue Boards help you to visualize
+and manage the entire process _in_ GitLab.
+
+With [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards), available
+only in [GitLab Enterprise Edition](https://about.gitlab.com/gitlab-ee/),
+you go even further, as you can not only keep yourself and your project
+organized from a broader perspective with one Issue Board per project,
+but also allow your team members to organize their own workflow by creating
+multiple Issue Boards within the same project.
+
+## Use cases
+
+GitLab Workflow allows you to discuss proposals in issues, categorize them
+with labels, and from there organize and prioritize them with Issue Boards.
+
+For example, let's consider this simplified development workflow:
+
+1. You have a repository hosting your app's codebase
+and your team actively contributing to code
+1. Your **backend** team starts working a new
+implementation, gathers feedback and approval, and pass it over to **frontend**
+1. When frontend is complete, the new feature is deployed to **staging** to be tested
+1. When successful, it is deployed to **production**
+
+If we have the labels "**backend**", "**frontend**", "**staging**", and
+"**production**", and an Issue Board with a list for each, we can:
+
+- Visualize the entire flow of implementations since the
+beginning of the development lifecycle until deployed to production
+- Prioritize the issues in a list by moving them vertically
+- Move issues between lists to organize them according to the labels you've set
+- Add multiple issues to lists in the board by selecting one or more existing issues
+
+![issue card moving](img/issue_board_move_issue_card_list.png)
+
+> **Notes:**
+>
+>- For a broader use case, please check the blog post
+[GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario).
+>
+>- For a real use case, please check why
+[Codepen decided to adopt Issue Boards](https://about.gitlab.com/2017/01/27/codepen-welcome-to-gitlab/#project-management-everything-in-one-place)
+to improve their workflow with [multiple boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards).
+
+## Issue Board terminology
Below is a table of the definitions used for GitLab's Issue Board.
@@ -57,7 +109,7 @@ In short, here's a list of actions you can take in an Issue Board:
If you are not able to perform one or more of the things above, make sure you
have the right [permissions](#permissions).
-## First time using the issue board
+## First time using the Issue Board
The first time you navigate to your Issue Board, you will be presented with
a default list (**Done**) and a welcoming message that gives
@@ -98,7 +150,7 @@ list view that is removed. You can always add it back later if you need.
## Adding issues to a list
You can add issues to a list by clicking the **Add issues** button that is
-present in the upper right corner of the issue board. This will open up a modal
+present in the upper right corner of the Issue Board. This will open up a modal
window where you can see all the issues that do not belong to any list.
Select one or more issues by clicking on the cards and then click **Add issues**
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index fe87e6f9495..e55e2aea023 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -1,4 +1,4 @@
-# Issues documentation
+# Issues
The GitLab Issue Tracker is an advanced and complete tool
for tracking the evolution of a new idea or the process
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index e10ccc4fc46..ea28968fbb2 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -300,7 +300,7 @@ If there are no merge conflicts and the feature branches are short lived the ris
If there are merge conflicts you merge the master branch into the feature branch and the CI server will rerun the tests.
If you have long lived feature branches that last for more than a few days you should make your issues smaller.
-## Working wih feature branches
+## Working with feature branches
![Shell output showing git pull output](git_pull.png)
diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature
deleted file mode 100644
index 046e2815d4e..00000000000
--- a/features/dashboard/new_project.feature
+++ /dev/null
@@ -1,30 +0,0 @@
-@dashboard
-Feature: New Project
-Background:
- Given I sign in as a user
- And I own project "Shop"
- And I visit dashboard page
- And I click "New project" link
-
- @javascript
- Scenario: I should see New Projects page
- Then I see "New Project" page
- Then I see all possible import options
-
- @javascript
- Scenario: I should see instructions on how to import from Git URL
- Given I see "New Project" page
- When I click on "Repo by URL"
- Then I see instructions on how to import from Git URL
-
- @javascript
- Scenario: I should see instructions on how to import from GitHub
- Given I see "New Project" page
- When I click on "Import project from GitHub"
- Then I am redirected to the GitHub import page
-
- @javascript
- Scenario: I should see Google Code import page
- Given I see "New Project" page
- When I click on "Google Code"
- Then I redirected to Google Code import page
diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb
deleted file mode 100644
index 530fd6f7bdb..00000000000
--- a/features/steps/dashboard/new_project.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-class Spinach::Features::NewProject < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
-
- step 'I click "New project" link' do
- page.within '#content-body' do
- click_link "New project"
- end
- end
-
- step 'I click "New project" in top right menu' do
- page.within '.header-content' do
- click_link "New project"
- end
- end
-
- step 'I see "New Project" page' do
- expect(page).to have_content('Project path')
- expect(page).to have_content('Project name')
- end
-
- step 'I see all possible import options' do
- expect(page).to have_link('GitHub')
- expect(page).to have_link('Bitbucket')
- expect(page).to have_link('GitLab.com')
- expect(page).to have_link('Google Code')
- expect(page).to have_button('Repo by URL')
- expect(page).to have_link('GitLab export')
- end
-
- step 'I click on "Import project from GitHub"' do
- first('.import_github').click
- end
-
- step 'I am redirected to the GitHub import page' do
- expect(page).to have_content('Import Projects from GitHub')
- expect(current_path).to eq new_import_github_path
- end
-
- step 'I click on "Repo by URL"' do
- first('.import_git').click
- end
-
- step 'I see instructions on how to import from Git URL' do
- git_import_instructions = first('.js-toggle-content')
- expect(git_import_instructions).to be_visible
- expect(git_import_instructions).to have_content "Git repository URL"
- end
-
- step 'I click on "Google Code"' do
- first('.import_google_code').click
- end
-
- step 'I redirected to Google Code import page' do
- expect(page).to have_content('Import projects from Google Code')
- expect(current_path).to eq new_import_google_code_path
- end
-end
diff --git a/features/steps/dashboard/starred_projects.rb b/features/steps/dashboard/starred_projects.rb
deleted file mode 100644
index c33813e550b..00000000000
--- a/features/steps/dashboard/starred_projects.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-class Spinach::Features::DashboardStarredProjects < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
-
- step 'I starred project "Community"' do
- current_user.toggle_star(Project.find_by(name: 'Community'))
- end
-
- step 'I should not see project "Shop"' do
- page.within '.projects-list' do
- expect(page).not_to have_content('Shop')
- end
- end
-end
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index 36fc315599e..2c59ec5bb06 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -232,7 +232,7 @@ module SharedDiffNote
end
def click_parallel_diff_line(code, line_type)
- find(".line_content.parallel.#{line_type}[data-line-code='#{code}']").trigger 'mouseover'
+ find(".line_holder.parallel .diff-line-num[id='#{code}']").trigger 'mouseover'
find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click'
end
end
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index c6fc17cc391..bcb842b9211 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -67,7 +67,7 @@ module API
result = ::Files::MultiService.new(user_project, current_user, attrs).execute
if result[:status] == :success
- commit_detail = user_project.repository.commits(result[:result], limit: 1).first
+ commit_detail = user_project.repository.commit(result[:result])
present commit_detail, with: Entities::RepoCommitDetail
else
render_api_error!(result[:message], 400)
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 886e97a2638..d0bd64b2972 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -1,4 +1,4 @@
-require 'declarative_policy'
+require_dependency 'declarative_policy'
module API
# Projects API
diff --git a/lib/api/users.rb b/lib/api/users.rb
index f9555842daf..5b9d9a71be4 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -4,10 +4,13 @@ module API
before do
allow_access_with_scope :read_user if request.get?
- authenticate!
end
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
+ before do
+ authenticate_non_get!
+ end
+
helpers do
def find_user(params)
id = params[:user_id] || params[:id]
@@ -51,15 +54,22 @@ module API
use :pagination
end
get do
- unless can?(current_user, :read_users_list)
- render_api_error!("Not authorized.", 403)
- end
-
authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?)
users = UsersFinder.new(current_user, params).execute
- entity = current_user.admin? ? Entities::UserWithAdmin : Entities::UserBasic
+ authorized = can?(current_user, :read_users_list)
+
+ # When `current_user` is not present, require that the `username`
+ # parameter is passed, to prevent an unauthenticated user from accessing
+ # a list of all the users on the GitLab instance. `UsersFinder` performs
+ # an exact match on the `username` parameter, so we are guaranteed to
+ # get either 0 or 1 `users` here.
+ authorized &&= params[:username].present? if current_user.blank?
+
+ forbidden!("Not authorized to access /api/v4/users") unless authorized
+
+ entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic
present paginate(users), with: entity
end
@@ -398,6 +408,10 @@ module API
end
resource :user do
+ before do
+ authenticate!
+ end
+
desc 'Get the currently authenticated user' do
success Entities::UserPublic
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 8bc2dd18bda..7a262dd025c 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -216,12 +216,7 @@ module Banzai
@references_per_project ||= begin
refs = Hash.new { |hash, key| hash[key] = Set.new }
- regex =
- if uses_reference_pattern?
- Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern)
- else
- object_class.link_reference_pattern
- end
+ regex = Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern)
nodes.each do |node|
node.to_html.scan(regex) do
@@ -323,14 +318,6 @@ module Banzai
value
end
end
-
- # There might be special cases like filters
- # that should ignore reference pattern
- # eg: IssueReferenceFilter when using a external issues tracker
- # In those cases this method should be overridden on the filter subclass
- def uses_reference_pattern?
- true
- end
end
end
end
diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb
index dce4de3ceaf..53a229256a5 100644
--- a/lib/banzai/filter/external_issue_reference_filter.rb
+++ b/lib/banzai/filter/external_issue_reference_filter.rb
@@ -3,6 +3,8 @@ module Banzai
# HTML filter that replaces external issue tracker references with links.
# References are ignored if the project doesn't use an external issue
# tracker.
+ #
+ # This filter does not support cross-project references.
class ExternalIssueReferenceFilter < ReferenceFilter
self.reference_type = :external_issue
@@ -87,7 +89,7 @@ module Banzai
end
def issue_reference_pattern
- external_issues_cached(:issue_reference_pattern)
+ external_issues_cached(:external_issue_reference_pattern)
end
private
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 044d18ff824..ba1a5ac84b3 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -15,10 +15,6 @@ module Banzai
Issue
end
- def uses_reference_pattern?
- context[:project].default_issues_tracker?
- end
-
def find_object(project, iid)
issues_per_project[project][iid]
end
@@ -38,13 +34,7 @@ module Banzai
projects_per_reference.each do |path, project|
issue_ids = references_per_project[path]
-
- issues =
- if project.default_issues_tracker?
- project.issues.where(iid: issue_ids.to_a)
- else
- issue_ids.map { |id| ExternalIssue.new(id, project) }
- end
+ issues = project.issues.where(iid: issue_ids.to_a)
issues.each do |issue|
hash[project][issue.iid.to_i] = issue
@@ -55,26 +45,6 @@ module Banzai
end
end
- def object_link_title(object)
- if object.is_a?(ExternalIssue)
- "Issue in #{object.project.external_issue_tracker.title}"
- else
- super
- end
- end
-
- def data_attributes_for(text, project, object, link: false)
- if object.is_a?(ExternalIssue)
- data_attribute(
- project: project.id,
- external_issue: object.id,
- reference_type: ExternalIssueReferenceFilter.reference_type
- )
- else
- super
- end
- end
-
def projects_relation_for_paths(paths)
super(paths).includes(:gitlab_issue_tracker_service)
end
diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb
index 9fd4bd68d43..a65bbe23958 100644
--- a/lib/banzai/reference_parser/issue_parser.rb
+++ b/lib/banzai/reference_parser/issue_parser.rb
@@ -4,9 +4,6 @@ module Banzai
self.reference_type = :issue
def nodes_visible_to_user(user, nodes)
- # It is not possible to check access rights for external issue trackers
- return nodes if project && project.external_issue_tracker
-
issues = issues_for_nodes(nodes)
readable_issues = Ability
diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb
index 897dcff8012..6555c589173 100644
--- a/lib/gitlab/ci/config/entry/image.rb
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -15,7 +15,7 @@ module Gitlab
validates :config, allowed_keys: ALLOWED_KEYS
validates :name, type: String, presence: true
- validates :entrypoint, type: String, allow_nil: true
+ validates :entrypoint, array_of_strings: true, allow_nil: true
end
def hash?
diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb
index b52faf48b58..3e2ebcff31a 100644
--- a/lib/gitlab/ci/config/entry/service.rb
+++ b/lib/gitlab/ci/config/entry/service.rb
@@ -15,8 +15,8 @@ module Gitlab
validates :config, allowed_keys: ALLOWED_KEYS
validates :name, type: String, presence: true
- validates :entrypoint, type: String, allow_nil: true
- validates :command, type: String, allow_nil: true
+ validates :entrypoint, array_of_strings: true, allow_nil: true
+ validates :command, array_of_strings: true, allow_nil: true
validates :alias, type: String, allow_nil: true
end
diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb
index bd90d24a2ec..5042916343b 100644
--- a/lib/gitlab/git/hook.rb
+++ b/lib/gitlab/git/hook.rb
@@ -4,9 +4,10 @@ module Gitlab
GL_PROTOCOL = 'web'.freeze
attr_reader :name, :repo_path, :path
- def initialize(name, repo_path)
+ def initialize(name, project)
@name = name
- @repo_path = repo_path
+ @project = project
+ @repo_path = project.repository.path
@path = File.join(repo_path.strip, 'hooks', name)
end
@@ -38,7 +39,8 @@ module Gitlab
vars = {
'GL_ID' => gl_id,
'PWD' => repo_path,
- 'GL_PROTOCOL' => GL_PROTOCOL
+ 'GL_PROTOCOL' => GL_PROTOCOL,
+ 'GL_REPOSITORY' => Gitlab::GlRepository.gl_repository(@project, false)
}
options = {
diff --git a/lib/gitlab/gitaly_client/ref.rb b/lib/gitlab/gitaly_client/ref.rb
index 2d61992f595..6edc69de078 100644
--- a/lib/gitlab/gitaly_client/ref.rb
+++ b/lib/gitlab/gitaly_client/ref.rb
@@ -34,7 +34,7 @@ module Gitlab
commit_id: commit_id,
prefix: ref_prefix
)
- GitalyClient.call(@storage, :ref, :find_ref_name, request).name
+ encode!(GitalyClient.call(@storage, :ref, :find_ref_name, request).name.dup)
end
def count_tag_names
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 319633656ff..2d1ae6a5925 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -2,11 +2,14 @@
module Gitlab
module GonHelper
+ include WebpackHelper
+
def add_gon_variables
gon.api_version = 'v4'
gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
gon.max_file_size = current_application_settings.max_attachment_size
gon.asset_host = ActionController::Base.asset_host
+ gon.webpack_public_path = webpack_public_path
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.shortcuts_path = help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
diff --git a/lib/gitlab/metrics/base_sampler.rb b/lib/gitlab/metrics/base_sampler.rb
new file mode 100644
index 00000000000..219accfc029
--- /dev/null
+++ b/lib/gitlab/metrics/base_sampler.rb
@@ -0,0 +1,94 @@
+require 'logger'
+module Gitlab
+ module Metrics
+ class BaseSampler
+ def self.initialize_instance(*args)
+ raise "#{name} singleton instance already initialized" if @instance
+ @instance = new(*args)
+ at_exit(&@instance.method(:stop))
+ @instance
+ end
+
+ def self.instance
+ @instance
+ end
+
+ attr_reader :running
+
+ # interval - The sampling interval in seconds.
+ def initialize(interval)
+ interval_half = interval.to_f / 2
+
+ @interval = interval
+ @interval_steps = (-interval_half..interval_half).step(0.1).to_a
+
+ @mutex = Mutex.new
+ end
+
+ def enabled?
+ true
+ end
+
+ def start
+ return unless enabled?
+
+ @mutex.synchronize do
+ return if running
+ @running = true
+
+ @thread = Thread.new do
+ sleep(sleep_interval)
+
+ while running
+ safe_sample
+
+ sleep(sleep_interval)
+ end
+ end
+ end
+ end
+
+ def stop
+ @mutex.synchronize do
+ return unless running
+
+ @running = false
+
+ if @thread
+ @thread.wakeup if @thread.alive?
+ @thread.join
+ @thread = nil
+ end
+ end
+ end
+
+ def safe_sample
+ sample
+ rescue => e
+ Rails.logger.warn("#{self.class}: #{e}, stopping")
+ stop
+ end
+
+ def sample
+ raise NotImplementedError
+ end
+
+ # Returns the sleep interval with a random adjustment.
+ #
+ # The random adjustment is put in place to ensure we:
+ #
+ # 1. Don't generate samples at the exact same interval every time (thus
+ # potentially missing anything that happens in between samples).
+ # 2. Don't sample data at the same interval two times in a row.
+ def sleep_interval
+ while step = @interval_steps.sample
+ if step != @last_step
+ @last_step = step
+
+ return @interval + @last_step
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/connection_rack_middleware.rb b/lib/gitlab/metrics/connection_rack_middleware.rb
new file mode 100644
index 00000000000..b3da360be8f
--- /dev/null
+++ b/lib/gitlab/metrics/connection_rack_middleware.rb
@@ -0,0 +1,45 @@
+module Gitlab
+ module Metrics
+ class ConnectionRackMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def self.rack_request_count
+ @rack_request_count ||= Gitlab::Metrics.counter(:rack_request, 'Rack request count')
+ end
+
+ def self.rack_response_count
+ @rack_response_count ||= Gitlab::Metrics.counter(:rack_response, 'Rack response count')
+ end
+
+ def self.rack_uncaught_errors_count
+ @rack_uncaught_errors_count ||= Gitlab::Metrics.counter(:rack_uncaught_errors, 'Rack connections handling uncaught errors count')
+ end
+
+ def self.rack_execution_time
+ @rack_execution_time ||= Gitlab::Metrics.histogram(:rack_execution_time, 'Rack connection handling execution time',
+ {}, [0.05, 0.1, 0.25, 0.5, 0.7, 1, 1.5, 2, 2.5, 3, 5, 7, 10])
+ end
+
+ def call(env)
+ method = env['REQUEST_METHOD'].downcase
+ started = Time.now.to_f
+ begin
+ ConnectionRackMiddleware.rack_request_count.increment(method: method)
+
+ status, headers, body = @app.call(env)
+
+ ConnectionRackMiddleware.rack_response_count.increment(method: method, status: status)
+ [status, headers, body]
+ rescue
+ ConnectionRackMiddleware.rack_uncaught_errors_count.increment
+ raise
+ ensure
+ elapsed = Time.now.to_f - started
+ ConnectionRackMiddleware.rack_execution_time.observe({}, elapsed)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/sampler.rb b/lib/gitlab/metrics/influx_sampler.rb
index 0000450d9bb..6db1dd755b7 100644
--- a/lib/gitlab/metrics/sampler.rb
+++ b/lib/gitlab/metrics/influx_sampler.rb
@@ -5,14 +5,11 @@ module Gitlab
# This class is used to gather statistics that can't be directly associated
# with a transaction such as system memory usage, garbage collection
# statistics, etc.
- class Sampler
+ class InfluxSampler < BaseSampler
# interval - The sampling interval in seconds.
def initialize(interval = Metrics.settings[:sample_interval])
- interval_half = interval.to_f / 2
-
- @interval = interval
- @interval_steps = (-interval_half..interval_half).step(0.1).to_a
- @last_step = nil
+ super(interval)
+ @last_step = nil
@metrics = []
@@ -26,18 +23,6 @@ module Gitlab
end
end
- def start
- Thread.new do
- Thread.current.abort_on_exception = true
-
- loop do
- sleep(sleep_interval)
-
- sample
- end
- end
- end
-
def sample
sample_memory_usage
sample_file_descriptors
@@ -86,7 +71,7 @@ module Gitlab
end
def sample_gc
- time = GC::Profiler.total_time * 1000.0
+ time = GC::Profiler.total_time * 1000.0
stats = GC.stat.merge(total_time: time)
# We want the difference of GC runs compared to the last sample, not the
@@ -111,23 +96,6 @@ module Gitlab
def sidekiq?
Sidekiq.server?
end
-
- # Returns the sleep interval with a random adjustment.
- #
- # The random adjustment is put in place to ensure we:
- #
- # 1. Don't generate samples at the exact same interval every time (thus
- # potentially missing anything that happens in between samples).
- # 2. Don't sample data at the same interval two times in a row.
- def sleep_interval
- while step = @interval_steps.sample
- if step != @last_step
- @last_step = step
-
- return @interval + @last_step
- end
- end
- end
end
end
end
diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb
index 9d314a56e58..fb7bbc7cfc7 100644
--- a/lib/gitlab/metrics/prometheus.rb
+++ b/lib/gitlab/metrics/prometheus.rb
@@ -29,8 +29,8 @@ module Gitlab
provide_metric(name) || registry.summary(name, docstring, base_labels)
end
- def gauge(name, docstring, base_labels = {})
- provide_metric(name) || registry.gauge(name, docstring, base_labels)
+ def gauge(name, docstring, base_labels = {}, multiprocess_mode = :all)
+ provide_metric(name) || registry.gauge(name, docstring, base_labels, multiprocess_mode)
end
def histogram(name, docstring, base_labels = {}, buckets = ::Prometheus::Client::Histogram::DEFAULT_BUCKETS)
diff --git a/lib/gitlab/metrics/unicorn_sampler.rb b/lib/gitlab/metrics/unicorn_sampler.rb
new file mode 100644
index 00000000000..f6987252039
--- /dev/null
+++ b/lib/gitlab/metrics/unicorn_sampler.rb
@@ -0,0 +1,48 @@
+module Gitlab
+ module Metrics
+ class UnicornSampler < BaseSampler
+ def initialize(interval)
+ super(interval)
+ end
+
+ def unicorn_active_connections
+ @unicorn_active_connections ||= Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max)
+ end
+
+ def unicorn_queued_connections
+ @unicorn_queued_connections ||= Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max)
+ end
+
+ def enabled?
+ # Raindrops::Linux.tcp_listener_stats is only present on Linux
+ unicorn_with_listeners? && Raindrops::Linux.respond_to?(:tcp_listener_stats)
+ end
+
+ def sample
+ Raindrops::Linux.tcp_listener_stats(tcp_listeners).each do |addr, stats|
+ unicorn_active_connections.set({ type: 'tcp', address: addr }, stats.active)
+ unicorn_queued_connections.set({ type: 'tcp', address: addr }, stats.queued)
+ end
+
+ Raindrops::Linux.unix_listener_stats(unix_listeners).each do |addr, stats|
+ unicorn_active_connections.set({ type: 'unix', address: addr }, stats.active)
+ unicorn_queued_connections.set({ type: 'unix', address: addr }, stats.queued)
+ end
+ end
+
+ private
+
+ def tcp_listeners
+ @tcp_listeners ||= Unicorn.listener_names.grep(%r{\A[^/]+:\d+\z})
+ end
+
+ def unix_listeners
+ @unix_listeners ||= Unicorn.listener_names - tcp_listeners
+ end
+
+ def unicorn_with_listeners?
+ defined?(Unicorn) && Unicorn.listener_names.any?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 22554236c38..0baea092e6a 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -2,6 +2,8 @@ require 'securerandom'
module Gitlab
class Shell
+ GITLAB_SHELL_ENV_VARS = %w(GIT_TERMINAL_PROMPT).freeze
+
Error = Class.new(StandardError)
KeyAdder = Struct.new(:io) do
@@ -67,8 +69,8 @@ module Gitlab
# add_repository("/path/to/storage", "gitlab/gitlab-ci")
#
def add_repository(storage, name)
- Gitlab::Utils.system_silent([gitlab_shell_projects_path,
- 'add-project', storage, "#{name}.git"])
+ gitlab_shell_fast_execute([gitlab_shell_projects_path,
+ 'add-project', storage, "#{name}.git"])
end
# Import repository
@@ -82,10 +84,9 @@ module Gitlab
def import_repository(storage, name, url)
# Timeout should be less than 900 ideally, to prevent the memory killer
# to silently kill the process without knowing we are timing out here.
- output, status = Popen.popen([gitlab_shell_projects_path, 'import-project',
- storage, "#{name}.git", url, "#{Gitlab.config.gitlab_shell.git_timeout}"])
- raise Error, output unless status.zero?
- true
+ cmd = [gitlab_shell_projects_path, 'import-project',
+ storage, "#{name}.git", url, "#{Gitlab.config.gitlab_shell.git_timeout}"]
+ gitlab_shell_fast_execute_raise_error(cmd)
end
# Fetch remote for repository
@@ -103,9 +104,7 @@ module Gitlab
args << '--force' if forced
args << '--no-tags' if no_tags
- output, status = Popen.popen(args)
- raise Error, output unless status.zero?
- true
+ gitlab_shell_fast_execute_raise_error(args)
end
# Move repository
@@ -117,8 +116,8 @@ module Gitlab
# mv_repository("/path/to/storage", "gitlab/gitlab-ci", "randx/gitlab-ci-new")
#
def mv_repository(storage, path, new_path)
- Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'mv-project',
- storage, "#{path}.git", "#{new_path}.git"])
+ gitlab_shell_fast_execute([gitlab_shell_projects_path, 'mv-project',
+ storage, "#{path}.git", "#{new_path}.git"])
end
# Fork repository to new namespace
@@ -131,9 +130,9 @@ module Gitlab
# fork_repository("/path/to/forked_from/storage", "gitlab/gitlab-ci", "/path/to/forked_to/storage", "randx")
#
def fork_repository(forked_from_storage, path, forked_to_storage, fork_namespace)
- Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'fork-project',
- forked_from_storage, "#{path}.git", forked_to_storage,
- fork_namespace])
+ gitlab_shell_fast_execute([gitlab_shell_projects_path, 'fork-project',
+ forked_from_storage, "#{path}.git", forked_to_storage,
+ fork_namespace])
end
# Remove repository from file system
@@ -145,8 +144,8 @@ module Gitlab
# remove_repository("/path/to/storage", "gitlab/gitlab-ci")
#
def remove_repository(storage, name)
- Gitlab::Utils.system_silent([gitlab_shell_projects_path,
- 'rm-project', storage, "#{name}.git"])
+ gitlab_shell_fast_execute([gitlab_shell_projects_path,
+ 'rm-project', storage, "#{name}.git"])
end
# Add new key to gitlab-shell
@@ -155,8 +154,8 @@ module Gitlab
# add_key("key-42", "sha-rsa ...")
#
def add_key(key_id, key_content)
- Gitlab::Utils.system_silent([gitlab_shell_keys_path,
- 'add-key', key_id, self.class.strip_key(key_content)])
+ gitlab_shell_fast_execute([gitlab_shell_keys_path,
+ 'add-key', key_id, self.class.strip_key(key_content)])
end
# Batch-add keys to authorized_keys
@@ -175,8 +174,10 @@ module Gitlab
# remove_key("key-342", "sha-rsa ...")
#
def remove_key(key_id, key_content)
- Gitlab::Utils.system_silent([gitlab_shell_keys_path,
- 'rm-key', key_id, key_content])
+ args = [gitlab_shell_keys_path, 'rm-key', key_id]
+ args << key_content if key_content
+
+ gitlab_shell_fast_execute(args)
end
# Remove all ssh keys from gitlab shell
@@ -185,7 +186,7 @@ module Gitlab
# remove_all_keys
#
def remove_all_keys
- Gitlab::Utils.system_silent([gitlab_shell_keys_path, 'clear'])
+ gitlab_shell_fast_execute([gitlab_shell_keys_path, 'clear'])
end
# Add empty directory for storing repositories
@@ -267,5 +268,31 @@ module Gitlab
def gitlab_shell_keys_path
File.join(gitlab_shell_path, 'bin', 'gitlab-keys')
end
+
+ private
+
+ def gitlab_shell_fast_execute(cmd)
+ output, status = gitlab_shell_fast_execute_helper(cmd)
+
+ return true if status.zero?
+
+ Rails.logger.error("gitlab-shell failed with error #{status}: #{output}")
+ false
+ end
+
+ def gitlab_shell_fast_execute_raise_error(cmd)
+ output, status = gitlab_shell_fast_execute_helper(cmd)
+
+ raise Error, output unless status.zero?
+ true
+ end
+
+ def gitlab_shell_fast_execute_helper(cmd)
+ vars = ENV.to_h.slice(*GITLAB_SHELL_ENV_VARS)
+
+ # Don't pass along the entire parent environment to prevent gitlab-shell
+ # from wasting I/O by searching through GEM_PATH
+ Bundler.with_original_env { Popen.popen(cmd, nil, vars) }
+ end
end
end
diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po
index afb8fb3176f..bda3fc09e85 100644
--- a/locale/en/gitlab.po
+++ b/locale/en/gitlab.po
@@ -17,9 +17,27 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"\n"
+msgid "%d additional commit has been omitted to prevent performance issues."
+msgid_plural "%d additional commits have been omitted to prevent performance issues."
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d commit"
+msgid_plural "%d commits"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr ""
+msgid "1 pipeline"
+msgid_plural "%d pipelines"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "A collection of graphs regarding Continuous Integration"
+msgstr ""
+
msgid "About auto deploy"
msgstr ""
@@ -61,9 +79,24 @@ msgstr[1] ""
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr ""
+msgid "BranchSwitcherPlaceholder|Search branches"
+msgstr ""
+
+msgid "BranchSwitcherTitle|Switch branch"
+msgstr ""
+
msgid "Branches"
msgstr ""
+msgid "Browse Directory"
+msgstr ""
+
+msgid "Browse File"
+msgstr ""
+
+msgid "Browse Files"
+msgstr ""
+
msgid "Browse files"
msgstr ""
@@ -159,6 +192,9 @@ msgid_plural "Commits"
msgstr[0] ""
msgstr[1] ""
+msgid "Commit duration in minutes for last 30 commits"
+msgstr ""
+
msgid "Commit message"
msgstr ""
@@ -171,6 +207,9 @@ msgstr ""
msgid "Commits"
msgstr ""
+msgid "Commits feed"
+msgstr ""
+
msgid "Commits|History"
msgstr ""
@@ -195,6 +234,9 @@ msgstr ""
msgid "Create New Directory"
msgstr ""
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr ""
+
msgid "Create directory"
msgstr ""
@@ -213,6 +255,9 @@ msgstr ""
msgid "CreateTag|Tag"
msgstr ""
+msgid "CreateTokenToCloneLink|create a personal access token"
+msgstr ""
+
msgid "Cron Timezone"
msgstr ""
@@ -323,6 +368,9 @@ msgstr ""
msgid "Files"
msgstr ""
+msgid "Filter by commit message"
+msgstr ""
+
msgid "Find by path"
msgstr ""
@@ -370,6 +418,15 @@ msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
+msgid "Jobs for last month"
+msgstr ""
+
+msgid "Jobs for last week"
+msgstr ""
+
+msgid "Jobs for last year"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr ""
@@ -535,6 +592,21 @@ msgstr ""
msgid "Pipeline Schedules"
msgstr ""
+msgid "PipelineCharts|Failed:"
+msgstr ""
+
+msgid "PipelineCharts|Overall statistics"
+msgstr ""
+
+msgid "PipelineCharts|Success ratio:"
+msgstr ""
+
+msgid "PipelineCharts|Successful:"
+msgstr ""
+
+msgid "PipelineCharts|Total:"
+msgstr ""
+
msgid "PipelineSchedules|Activated"
msgstr ""
@@ -565,6 +637,18 @@ msgstr ""
msgid "PipelineSheduleIntervalPattern|Custom"
msgstr ""
+msgid "Pipelines"
+msgstr ""
+
+msgid "Pipelines charts"
+msgstr ""
+
+msgid "Pipeline|all"
+msgstr ""
+
+msgid "Pipeline|success"
+msgstr ""
+
msgid "Pipeline|with stage"
msgstr ""
@@ -688,7 +772,7 @@ msgstr ""
msgid "Select target branch"
msgstr ""
-msgid "Set a password on your account to pull or push via %{protocol}"
+msgid "Set a password on your account to pull or push via %{protocol}."
msgstr ""
msgid "Set up CI"
@@ -714,10 +798,7 @@ msgstr ""
msgid "StarProject|Star"
msgstr ""
-msgid "Start a %{new_merge_request} with these changes"
-msgstr ""
-
-msgid "Start a <strong>new merge request</strong> with these changes"
+msgid "Start a %{new_merge_request} with these changes"
msgstr ""
msgid "Switch branch/tag"
@@ -948,9 +1029,15 @@ msgstr ""
msgid "Upload file"
msgstr ""
+msgid "UploadLink|click to upload"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr ""
+msgid "View open merge request"
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr ""
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 07f9efeb495..9f1caeddaa7 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-06-19 15:50-0500\n"
-"PO-Revision-Date: 2017-06-19 15:50-0500\n"
+"POT-Creation-Date: 2017-06-28 13:32+0200\n"
+"PO-Revision-Date: 2017-06-28 13:32+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
-msgid "%d additional commit have been omitted to prevent performance issues."
+msgid "%d additional commit has been omitted to prevent performance issues."
msgid_plural "%d additional commits have been omitted to prevent performance issues."
msgstr[0] ""
msgstr[1] ""
@@ -31,6 +31,14 @@ msgstr[1] ""
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr ""
+msgid "1 pipeline"
+msgid_plural "%d pipelines"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "A collection of graphs regarding Continuous Integration"
+msgstr ""
+
msgid "About auto deploy"
msgstr ""
@@ -185,6 +193,9 @@ msgid_plural "Commits"
msgstr[0] ""
msgstr[1] ""
+msgid "Commit duration in minutes for last 30 commits"
+msgstr ""
+
msgid "Commit message"
msgstr ""
@@ -224,6 +235,9 @@ msgstr ""
msgid "Create New Directory"
msgstr ""
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr ""
+
msgid "Create directory"
msgstr ""
@@ -242,6 +256,9 @@ msgstr ""
msgid "CreateTag|Tag"
msgstr ""
+msgid "CreateTokenToCloneLink|create a personal access token"
+msgstr ""
+
msgid "Cron Timezone"
msgstr ""
@@ -402,6 +419,15 @@ msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
+msgid "Jobs for last month"
+msgstr ""
+
+msgid "Jobs for last week"
+msgstr ""
+
+msgid "Jobs for last year"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr ""
@@ -567,6 +593,21 @@ msgstr ""
msgid "Pipeline Schedules"
msgstr ""
+msgid "PipelineCharts|Failed:"
+msgstr ""
+
+msgid "PipelineCharts|Overall statistics"
+msgstr ""
+
+msgid "PipelineCharts|Success ratio:"
+msgstr ""
+
+msgid "PipelineCharts|Successful:"
+msgstr ""
+
+msgid "PipelineCharts|Total:"
+msgstr ""
+
msgid "PipelineSchedules|Activated"
msgstr ""
@@ -597,6 +638,18 @@ msgstr ""
msgid "PipelineSheduleIntervalPattern|Custom"
msgstr ""
+msgid "Pipelines"
+msgstr ""
+
+msgid "Pipelines charts"
+msgstr ""
+
+msgid "Pipeline|all"
+msgstr ""
+
+msgid "Pipeline|success"
+msgstr ""
+
msgid "Pipeline|with stage"
msgstr ""
@@ -720,7 +773,7 @@ msgstr ""
msgid "Select target branch"
msgstr ""
-msgid "Set a password on your account to pull or push via %{protocol}"
+msgid "Set a password on your account to pull or push via %{protocol}."
msgstr ""
msgid "Set up CI"
diff --git a/package.json b/package.json
index 045f07ee2f9..5a997e813f8 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
},
"dependencies": {
"babel-core": "^6.22.1",
+ "babel-eslint": "^7.2.1",
"babel-loader": "^6.2.10",
"babel-plugin-transform-define": "^1.2.0",
"babel-preset-latest": "^6.24.0",
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index f3263bc177d..c6e5fb61cf9 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -23,6 +23,21 @@ describe Groups::MilestonesController do
project.team << [user, :master]
end
+ describe "#index" do
+ it 'shows group milestones page' do
+ get :index, group_id: group.to_param
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'shows group milestones JSON' do
+ get :index, group_id: group.to_param, format: :json
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type).to eq 'application/json'
+ end
+ end
+
it_behaves_like 'milestone tabs'
describe "#create" do
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index aef1c17a239..1bb2db11e7f 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -220,7 +220,7 @@ FactoryGirl.define do
active: true,
properties: {
'project_url' => 'http://redmine/projects/project_name_in_redmine',
- 'issues_url' => "http://redmine/#{project.id}/project_name_in_redmine/:id",
+ 'issues_url' => 'http://redmine/projects/project_name_in_redmine/issues/:id',
'new_issue_url' => 'http://redmine/projects/project_name_in_redmine/issues/new'
}
)
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 301c243febd..1c9595def21 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -79,6 +79,22 @@ describe 'Issue Boards', feature: true, js: true do
end
end
+ it 'does not show remove button for backlog or closed issues' do
+ create(:issue, project: project)
+ create(:issue, :closed, project: project)
+
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_requests
+
+ click_card(find('.board:nth-child(1)').first('.card'))
+
+ expect(find('.issue-boards-sidebar')).not_to have_button 'Remove from board'
+
+ click_card(find('.board:nth-child(3)').first('.card'))
+
+ expect(find('.issue-boards-sidebar')).not_to have_button 'Remove from board'
+ end
+
context 'assignee' do
it 'updates the issues assignee' do
click_card(card)
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index f29186f368d..e9ef5d7983a 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Projects', feature: true do
+feature 'Dashboard Projects' do
let(:user) { create(:user) }
- let(:project) { create(:project, name: "awesome stuff") }
+ let(:project) { create(:project, name: 'awesome stuff') }
let(:project2) { create(:project, :public, name: 'Community project') }
before do
@@ -15,6 +15,14 @@ RSpec.describe 'Dashboard Projects', feature: true do
expect(page).to have_content('awesome stuff')
end
+ it 'shows "New project" button' do
+ visit dashboard_projects_path
+
+ page.within '#content-body' do
+ expect(page).to have_link('New project')
+ end
+ end
+
context 'when last_repository_updated_at, last_activity_at and update_at are present' do
it 'shows the last_repository_updated_at attribute as the update date' do
project.update_attributes!(last_repository_updated_at: Time.now, last_activity_at: 1.hour.ago)
@@ -47,8 +55,8 @@ RSpec.describe 'Dashboard Projects', feature: true do
end
end
- describe "with a pipeline", redis: true do
- let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) }
+ describe 'with a pipeline', redis: true do
+ let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) }
before do
# Since the cache isn't updated when a new pipeline is created
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index ea749528c11..d492a15ea17 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -129,7 +129,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do
before do
large_diff.find('.diff-line-num', match: :prefer_exact).hover
- large_diff.find('.add-diff-note').click
+ large_diff.find('.add-diff-note', match: :prefer_exact).click
large_diff.find('.note-textarea').send_keys comment_text
large_diff.find_button('Comment').click
wait_for_requests
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index ecacca00a61..c1dc7be7088 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -135,7 +135,7 @@ feature 'Group', feature: true do
expect(page).not_to have_content('secret-group')
end
- describe 'group edit' do
+ describe 'group edit', js: true do
let(:group) { create(:group) }
let(:path) { edit_group_path(group) }
let(:new_name) { 'new-name' }
@@ -157,8 +157,8 @@ feature 'Group', feature: true do
end
it 'removes group' do
- click_link 'Remove group'
-
+ expect { remove_with_confirm('Remove group', group.path) }.to change {Group.count}.by(-1)
+ expect(group.members.all.count).to be_zero
expect(page).to have_content "scheduled for deletion"
end
end
@@ -212,4 +212,10 @@ feature 'Group', feature: true do
expect(page).to have_content(nested_group.name)
end
end
+
+ def remove_with_confirm(button_text, confirm_with)
+ click_button button_text
+ fill_in 'confirm_name_input', with: confirm_with
+ click_button 'Confirm'
+ end
end
diff --git a/spec/features/issuables/user_sees_sidebar_spec.rb b/spec/features/issuables/user_sees_sidebar_spec.rb
new file mode 100644
index 00000000000..4d7a7dc1806
--- /dev/null
+++ b/spec/features/issuables/user_sees_sidebar_spec.rb
@@ -0,0 +1,30 @@
+require 'rails_helper'
+
+describe 'Issue Sidebar on Mobile' do
+ include MobileHelpers
+
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:issue) { create(:issue, project: project) }
+ let!(:user) { create(:user)}
+
+ before do
+ sign_in(user)
+ end
+
+ context 'mobile sidebar on merge requests', js: true do
+ before do
+ visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
+ end
+
+ it_behaves_like "issue sidebar stays collapsed on mobile"
+ end
+
+ context 'mobile sidebar on issues', js: true do
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it_behaves_like "issue sidebar stays collapsed on mobile"
+ end
+end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index b369ef1ff79..58f6bd277e4 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -31,8 +31,8 @@ describe 'New/edit issue', :feature, :js do
# the original method, resulting in infinite recurison when called.
# This is likely a bug with helper modules included into dynamically generated view classes.
# To work around this, we have to hold on to and call to the original implementation manually.
- original_issue_dropdown_options = FormHelper.instance_method(:issue_dropdown_options)
- allow_any_instance_of(FormHelper).to receive(:issue_dropdown_options).and_wrap_original do |original, *args|
+ original_issue_dropdown_options = FormHelper.instance_method(:issue_assignees_dropdown_options)
+ allow_any_instance_of(FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args|
options = original_issue_dropdown_options.bind(original.receiver).call(*args)
options[:data][:per_page] = 2
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 163bc4bb32f..09724781a27 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -154,20 +154,6 @@ feature 'Issue Sidebar', feature: true do
end
end
- context 'as a allowed mobile user', js: true do
- before do
- project.team << [user, :developer]
- resize_screen_xs
- visit_issue(project, issue)
- end
-
- context 'mobile sidebar' do
- it 'collapses the sidebar for small screens' do
- expect(page).not_to have_css('aside.right-sidebar.right-sidebar-collapsed')
- end
- end
- end
-
context 'as a guest' do
before do
project.team << [user, :guest]
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 21a7637fe7f..d6e78157e5f 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -41,13 +41,10 @@ feature 'issue move to another project' do
find('#issuable-move', visible: false).set(new_project.id)
click_button('Save changes')
- wait_for_requests
-
- expect(current_url).to include project_path(new_project)
-
expect(page).to have_content("Text with #{cross_reference}#{mr.to_reference}")
expect(page).to have_content("moved from #{cross_reference}#{issue.to_reference}")
expect(page).to have_content(issue.title)
+ expect(page.current_path).to include project_path(new_project)
end
scenario 'searching project dropdown', js: true do
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index dc981406e4e..df704b55839 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -1,16 +1,16 @@
require 'rails_helper'
-feature 'Multiple issue updating from issues#index', feature: true do
+feature 'Multiple issue updating from issues#index', :js do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
before do
project.team << [user, :master]
- gitlab_sign_in(user)
+ sign_in(user)
end
- context 'status', js: true do
+ context 'status' do
it 'sets to closed' do
visit namespace_project_issues_path(project.namespace, project)
@@ -37,7 +37,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
end
end
- context 'assignee', js: true do
+ context 'assignee' do
it 'updates to current user' do
visit namespace_project_issues_path(project.namespace, project)
@@ -67,8 +67,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
end
end
- context 'milestone', js: true do
- let(:milestone) { create(:milestone, project: project) }
+ context 'milestone' do
+ let!(:milestone) { create(:milestone, project: project) }
it 'updates milestone' do
visit namespace_project_issues_path(project.namespace, project)
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index a111aa87c52..3f8d2255298 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -98,6 +98,6 @@ feature 'Import/Export - project import integration test', feature: true, js: tr
end
def project_hook_exists?(project)
- Gitlab::Git::Hook.new('post-receive', project.repository.path).exists?
+ Gitlab::Git::Hook.new('post-receive', project).exists?
end
end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index 37d9a97033b..22fb1223739 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -1,13 +1,27 @@
-require "spec_helper"
+require 'spec_helper'
-feature "New project", feature: true do
+feature 'New project' do
let(:user) { create(:admin) }
before do
- gitlab_sign_in(user)
+ sign_in(user)
end
- context "Visibility level selector" do
+ it 'shows "New project" page' do
+ visit new_project_path
+
+ expect(page).to have_content('Project path')
+ expect(page).to have_content('Project name')
+
+ expect(page).to have_link('GitHub')
+ expect(page).to have_link('Bitbucket')
+ expect(page).to have_link('GitLab.com')
+ expect(page).to have_link('Google Code')
+ expect(page).to have_button('Repo by URL')
+ expect(page).to have_link('GitLab export')
+ end
+
+ context 'Visibility level selector' do
Gitlab::VisibilityLevel.options.each do |key, level|
it "sets selector to #{key}" do
stub_application_setting(default_project_visibility: level)
@@ -28,20 +42,20 @@ feature "New project", feature: true do
end
end
- context "Namespace selector" do
- context "with user namespace" do
+ context 'Namespace selector' do
+ context 'with user namespace' do
before do
visit new_project_path
end
- it "selects the user namespace" do
- namespace = find("#project_namespace_id")
+ it 'selects the user namespace' do
+ namespace = find('#project_namespace_id')
expect(namespace.text).to eq user.username
end
end
- context "with group namespace" do
+ context 'with group namespace' do
let(:group) { create(:group, :private, owner: user) }
before do
@@ -49,13 +63,13 @@ feature "New project", feature: true do
visit new_project_path(namespace_id: group.id)
end
- it "selects the group namespace" do
- namespace = find("#project_namespace_id option[selected]")
+ it 'selects the group namespace' do
+ namespace = find('#project_namespace_id option[selected]')
expect(namespace.text).to eq group.name
end
- context "on validation error" do
+ context 'on validation error' do
before do
fill_in('project_path', with: 'private-group-project')
choose('Internal')
@@ -64,15 +78,15 @@ feature "New project", feature: true do
expect(page).to have_css '.project-edit-errors .alert.alert-danger'
end
- it "selects the group namespace" do
- namespace = find("#project_namespace_id option[selected]")
+ it 'selects the group namespace' do
+ namespace = find('#project_namespace_id option[selected]')
expect(namespace.text).to eq group.name
end
end
end
- context "with subgroup namespace" do
+ context 'with subgroup namespace' do
let(:group) { create(:group, :private, owner: user) }
let(:subgroup) { create(:group, parent: group) }
@@ -81,8 +95,8 @@ feature "New project", feature: true do
visit new_project_path(namespace_id: subgroup.id)
end
- it "selects the group namespace" do
- namespace = find("#project_namespace_id option[selected]")
+ it 'selects the group namespace' do
+ namespace = find('#project_namespace_id option[selected]')
expect(namespace.text).to eq subgroup.full_path
end
@@ -94,10 +108,45 @@ feature "New project", feature: true do
visit new_project_path
end
- it 'does not autocomplete sensitive git repo URL' do
- autocomplete = find('#project_import_url')['autocomplete']
+ context 'from git repository url' do
+ before do
+ first('.import_git').click
+ end
+
+ it 'does not autocomplete sensitive git repo URL' do
+ autocomplete = find('#project_import_url')['autocomplete']
+
+ expect(autocomplete).to eq('off')
+ end
+
+ it 'shows import instructions' do
+ git_import_instructions = first('.js-toggle-content')
- expect(autocomplete).to eq('off')
+ expect(git_import_instructions).to be_visible
+ expect(git_import_instructions).to have_content 'Git repository URL'
+ end
+ end
+
+ context 'from GitHub' do
+ before do
+ first('.import_github').click
+ end
+
+ it 'shows import instructions' do
+ expect(page).to have_content('Import Projects from GitHub')
+ expect(current_path).to eq new_import_github_path
+ end
+ end
+
+ context 'from Google Code' do
+ before do
+ first('.import_google_code').click
+ end
+
+ it 'shows import instructions' do
+ expect(page).to have_content('Import projects from Google Code')
+ expect(current_path).to eq new_import_google_code_path
+ end
end
end
end
diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb
index 1bd7e038939..24fff1a3052 100644
--- a/spec/features/user_can_display_performance_bar_spec.rb
+++ b/spec/features/user_can_display_performance_bar_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'User can display performacne bar', :js do
+describe 'User can display performance bar', :js do
shared_examples 'performance bar is disabled' do
it 'does not show the performance bar by default' do
expect(page).not_to have_css('#peek')
@@ -27,8 +27,8 @@ describe 'User can display performacne bar', :js do
find('body').native.send_keys('pb')
end
- it 'does not show the performance bar by default' do
- expect(page).not_to have_css('#peek')
+ it 'shows the performance bar' do
+ expect(page).to have_css('#peek')
end
end
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 8ace1fb5751..4a52f0d5c58 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -295,22 +295,121 @@ describe IssuesFinder do
end
end
- describe '.not_restricted_by_confidentiality' do
- let(:authorized_user) { create(:user) }
- let(:project) { create(:empty_project, namespace: authorized_user.namespace) }
- let!(:public_issue) { create(:issue, project: project) }
- let!(:confidential_issue) { create(:issue, project: project, confidential: true) }
-
- it 'returns non confidential issues for nil user' do
- expect(described_class.send(:not_restricted_by_confidentiality, nil)).to include(public_issue)
- end
+ describe '#with_confidentiality_access_check' do
+ let(:guest) { create(:user) }
+ set(:authorized_user) { create(:user) }
+ set(:project) { create(:empty_project, namespace: authorized_user.namespace) }
+ set(:public_issue) { create(:issue, project: project) }
+ set(:confidential_issue) { create(:issue, project: project, confidential: true) }
+
+ context 'when no project filter is given' do
+ let(:params) { {} }
+
+ context 'for an anonymous user' do
+ subject { described_class.new(nil, params).with_confidentiality_access_check }
+
+ it 'returns only public issues' do
+ expect(subject).to include(public_issue)
+ expect(subject).not_to include(confidential_issue)
+ end
+ end
+
+ context 'for a user without project membership' do
+ subject { described_class.new(user, params).with_confidentiality_access_check }
+
+ it 'returns only public issues' do
+ expect(subject).to include(public_issue)
+ expect(subject).not_to include(confidential_issue)
+ end
+ end
+
+ context 'for a guest user' do
+ subject { described_class.new(guest, params).with_confidentiality_access_check }
+
+ before do
+ project.add_guest(guest)
+ end
+
+ it 'returns only public issues' do
+ expect(subject).to include(public_issue)
+ expect(subject).not_to include(confidential_issue)
+ end
+ end
+
+ context 'for a project member with access to view confidential issues' do
+ subject { described_class.new(authorized_user, params).with_confidentiality_access_check }
- it 'returns non confidential issues for user not authorized for the issues projects' do
- expect(described_class.send(:not_restricted_by_confidentiality, user)).to include(public_issue)
+ it 'returns all issues' do
+ expect(subject).to include(public_issue, confidential_issue)
+ end
+ end
end
- it 'returns all issues for user authorized for the issues projects' do
- expect(described_class.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue)
+ context 'when searching within a specific project' do
+ let(:params) { { project_id: project.id } }
+
+ context 'for an anonymous user' do
+ subject { described_class.new(nil, params).with_confidentiality_access_check }
+
+ it 'returns only public issues' do
+ expect(subject).to include(public_issue)
+ expect(subject).not_to include(confidential_issue)
+ end
+
+ it 'does not filter by confidentiality' do
+ expect(Issue).not_to receive(:where).with(a_string_matching('confidential'), anything)
+
+ subject
+ end
+ end
+
+ context 'for a user without project membership' do
+ subject { described_class.new(user, params).with_confidentiality_access_check }
+
+ it 'returns only public issues' do
+ expect(subject).to include(public_issue)
+ expect(subject).not_to include(confidential_issue)
+ end
+
+ it 'filters by confidentiality' do
+ expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything)
+
+ subject
+ end
+ end
+
+ context 'for a guest user' do
+ subject { described_class.new(guest, params).with_confidentiality_access_check }
+
+ before do
+ project.add_guest(guest)
+ end
+
+ it 'returns only public issues' do
+ expect(subject).to include(public_issue)
+ expect(subject).not_to include(confidential_issue)
+ end
+
+ it 'filters by confidentiality' do
+ expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything)
+
+ subject
+ end
+ end
+
+ context 'for a project member with access to view confidential issues' do
+ subject { described_class.new(authorized_user, params).with_confidentiality_access_check }
+
+ it 'returns all issues' do
+ expect(subject).to include(public_issue, confidential_issue)
+ end
+
+ it 'does not filter by confidentiality' do
+ expect(Issue).not_to receive(:where).with(a_string_matching('confidential'), anything)
+
+ subject
+ end
+ end
end
end
end
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
index 1724cdba830..95d96354b77 100644
--- a/spec/finders/labels_finder_spec.rb
+++ b/spec/finders/labels_finder_spec.rb
@@ -49,12 +49,12 @@ describe LabelsFinder do
end
context 'filtering by group_id' do
- it 'returns labels available for any project within the group' do
+ it 'returns labels available for any non-archived project within the group' do
group_1.add_developer(user)
-
+ project_1.archive!
finder = described_class.new(user, group_id: group_1.id)
- expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1, project_label_5]
+ expect(finder.execute).to eq [group_label_2, group_label_1, project_label_5]
end
end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 8da22dc78fa..e3f9d9db9eb 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -91,7 +91,7 @@ describe GroupsHelper do
let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
it 'outputs the groups in the correct order' do
- expect(group_title(very_deep_nested_group)).to match(/>#{group.name}<\/a>.*>#{nested_group.name}<\/a>.*>#{deep_nested_group.name}<\/a>/)
+ expect(helper.group_title(very_deep_nested_group)).to match(/>#{group.name}<\/a>.*>#{nested_group.name}<\/a>.*>#{deep_nested_group.name}<\/a>/)
end
end
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 15cb620199d..d2e918ef014 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -77,54 +77,89 @@ describe IssuablesHelper do
}.with_indifferent_access
end
+ let(:issues_finder) { IssuesFinder.new(nil, params) }
+ let(:merge_requests_finder) { MergeRequestsFinder.new(nil, params) }
+
+ before do
+ allow(helper).to receive(:issues_finder).and_return(issues_finder)
+ allow(helper).to receive(:merge_requests_finder).and_return(merge_requests_finder)
+ end
+
it 'returns the cached value when called for the same issuable type & with the same params' do
- expect(helper).to receive(:params).twice.and_return(params)
- expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
+ expect(issues_finder).to receive(:count_by_state).and_return(opened: 42)
expect(helper.issuables_state_counter_text(:issues, :opened))
.to eq('<span>Open</span> <span class="badge">42</span>')
- expect(helper).not_to receive(:issuables_count_for_state)
+ expect(issues_finder).not_to receive(:count_by_state)
expect(helper.issuables_state_counter_text(:issues, :opened))
.to eq('<span>Open</span> <span class="badge">42</span>')
end
+ it 'takes confidential status into account when searching for issues' do
+ expect(issues_finder).to receive(:count_by_state).and_return(opened: 42)
+
+ expect(helper.issuables_state_counter_text(:issues, :opened))
+ .to include('42')
+
+ expect(issues_finder).to receive(:user_cannot_see_confidential_issues?).twice.and_return(false)
+ expect(issues_finder).to receive(:count_by_state).and_return(opened: 40)
+
+ expect(helper.issuables_state_counter_text(:issues, :opened))
+ .to include('40')
+
+ expect(issues_finder).to receive(:user_can_see_all_confidential_issues?).and_return(true)
+ expect(issues_finder).to receive(:count_by_state).and_return(opened: 45)
+
+ expect(helper.issuables_state_counter_text(:issues, :opened))
+ .to include('45')
+ end
+
+ it 'does not take confidential status into account when searching for merge requests' do
+ expect(merge_requests_finder).to receive(:count_by_state).and_return(opened: 42)
+ expect(merge_requests_finder).not_to receive(:user_cannot_see_confidential_issues?)
+ expect(merge_requests_finder).not_to receive(:user_can_see_all_confidential_issues?)
+
+ expect(helper.issuables_state_counter_text(:merge_requests, :opened))
+ .to include('42')
+ end
+
it 'does not take some keys into account in the cache key' do
- expect(helper).to receive(:params).and_return({
+ expect(issues_finder).to receive(:count_by_state).and_return(opened: 42)
+ expect(issues_finder).to receive(:params).and_return({
author_id: '11',
state: 'foo',
sort: 'foo',
utf8: 'foo',
page: 'foo'
}.with_indifferent_access)
- expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
expect(helper.issuables_state_counter_text(:issues, :opened))
.to eq('<span>Open</span> <span class="badge">42</span>')
- expect(helper).to receive(:params).and_return({
+ expect(issues_finder).not_to receive(:count_by_state)
+ expect(issues_finder).to receive(:params).and_return({
author_id: '11',
state: 'bar',
sort: 'bar',
utf8: 'bar',
page: 'bar'
}.with_indifferent_access)
- expect(helper).not_to receive(:issuables_count_for_state)
expect(helper.issuables_state_counter_text(:issues, :opened))
.to eq('<span>Open</span> <span class="badge">42</span>')
end
it 'does not take params order into account in the cache key' do
- expect(helper).to receive(:params).and_return('author_id' => '11', 'state' => 'opened')
- expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
+ expect(issues_finder).to receive(:params).and_return('author_id' => '11', 'state' => 'opened')
+ expect(issues_finder).to receive(:count_by_state).and_return(opened: 42)
expect(helper.issuables_state_counter_text(:issues, :opened))
.to eq('<span>Open</span> <span class="badge">42</span>')
- expect(helper).to receive(:params).and_return('state' => 'opened', 'author_id' => '11')
- expect(helper).not_to receive(:issuables_count_for_state)
+ expect(issues_finder).to receive(:params).and_return('state' => 'opened', 'author_id' => '11')
+ expect(issues_finder).not_to receive(:count_by_state)
expect(helper.issuables_state_counter_text(:issues, :opened))
.to eq('<span>Open</span> <span class="badge">42</span>')
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
index 3cb809d42b5..24d4f1b4938 100644
--- a/spec/helpers/milestones_helper_spec.rb
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -1,6 +1,42 @@
require 'spec_helper'
describe MilestonesHelper do
+ describe '#milestones_filter_dropdown_path' do
+ let(:project) { create(:empty_project) }
+ let(:project2) { create(:empty_project) }
+ let(:group) { create(:group) }
+
+ context 'when @project present' do
+ it 'returns project milestones JSON URL' do
+ assign(:project, project)
+
+ expect(helper.milestones_filter_dropdown_path).to eq(namespace_project_milestones_path(project.namespace, project, :json))
+ end
+ end
+
+ context 'when @target_project present' do
+ it 'returns targeted project milestones JSON URL' do
+ assign(:target_project, project2)
+
+ expect(helper.milestones_filter_dropdown_path).to eq(namespace_project_milestones_path(project2.namespace, project2, :json))
+ end
+ end
+
+ context 'when @group present' do
+ it 'returns group milestones JSON URL' do
+ assign(:group, group)
+
+ expect(helper.milestones_filter_dropdown_path).to eq(group_milestones_path(group, :json))
+ end
+ end
+
+ context 'when neither of @project/@target_project/@group present' do
+ it 'returns dashboard milestones JSON URL' do
+ expect(helper.milestones_filter_dropdown_path).to eq(dashboard_milestones_path(:json))
+ end
+ end
+ end
+
describe "#milestone_date_range" do
def result_for(*args)
milestone_date_range(build(:milestone, *args))
diff --git a/spec/initializers/8_metrics_spec.rb b/spec/initializers/8_metrics_spec.rb
index a507d7f7f2b..d4189f902fd 100644
--- a/spec/initializers/8_metrics_spec.rb
+++ b/spec/initializers/8_metrics_spec.rb
@@ -1,17 +1,25 @@
require 'spec_helper'
-require_relative '../../config/initializers/8_metrics'
describe 'instrument_classes', lib: true do
let(:config) { double(:config) }
+ let(:unicorn_sampler) { double(:unicorn_sampler) }
+ let(:influx_sampler) { double(:influx_sampler) }
+
before do
allow(config).to receive(:instrument_method)
allow(config).to receive(:instrument_methods)
allow(config).to receive(:instrument_instance_method)
allow(config).to receive(:instrument_instance_methods)
+ allow(Gitlab::Metrics::UnicornSampler).to receive(:initialize_instance).and_return(unicorn_sampler)
+ allow(Gitlab::Metrics::InfluxSampler).to receive(:initialize_instance).and_return(influx_sampler)
+ allow(unicorn_sampler).to receive(:start)
+ allow(influx_sampler).to receive(:start)
+ allow(Gitlab::Application).to receive(:configure)
end
it 'can autoload and instrument all files' do
+ require_relative '../../config/initializers/8_metrics'
expect { instrument_classes(config) }.not_to raise_error
end
end
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 3fc03324d16..8e056882108 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,7 +1,7 @@
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */
import Cookies from 'js-cookie';
-import AwardsHandler from '~/awards_handler';
+import loadAwardsHandler from '~/awards_handler';
import '~/lib/utils/common_utils';
@@ -26,14 +26,13 @@ import '~/lib/utils/common_utils';
describe('AwardsHandler', function() {
preloadFixtures('issues/issue_with_comment.html.raw');
- beforeEach(function() {
+ beforeEach(function(done) {
loadFixtures('issues/issue_with_comment.html.raw');
- awardsHandler = new AwardsHandler;
- spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
- return function(button, url, emoji, cb) {
- return cb();
- };
- })(this));
+ loadAwardsHandler(true).then((obj) => {
+ awardsHandler = obj;
+ spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb());
+ done();
+ }).catch(fail);
let isEmojiMenuBuilt = false;
openAndWaitForEmojiMenu = function() {
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index 9df92318864..bc13373a27e 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -42,9 +42,6 @@ describe('Issuable output', () => {
}).$mount();
});
- afterEach(() => {
- });
-
it('should render a title/description/edited and update title/description/edited on update', (done) => {
vm.poll.options.successCallback({
json() {
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 6f4cb989847..56d938e1fbe 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -13,7 +13,7 @@ const metricsGroupsAPIResponse = {
'queries': [
{
'query_range': 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20',
- 'label': 'Container memory',
+ 'y_label': 'Memory',
'unit': 'MiB',
'result': [
{
@@ -2477,7 +2477,7 @@ export const singleRowMetrics = [
{
'title': 'CPU usage',
'weight': 1,
- 'y_label': 'Values',
+ 'y_label': 'Memory',
'queries': [
{
'query_range': 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100',
diff --git a/spec/javascripts/monitoring/monitoring_column_spec.js b/spec/javascripts/monitoring/monitoring_column_spec.js
index c8787f9708c..b3bc97adc9f 100644
--- a/spec/javascripts/monitoring/monitoring_column_spec.js
+++ b/spec/javascripts/monitoring/monitoring_column_spec.js
@@ -94,4 +94,15 @@ describe('MonitoringColumn', () => {
done();
});
});
+
+ it('has a title for the y-axis that comes from the backend', () => {
+ const component = createComponent({
+ columnData: singleRowMetrics[0],
+ classType: 'col-md-6',
+ updateAspectRatio: false,
+ deploymentData,
+ });
+
+ expect(component.yAxisLabel).toEqual(component.columnData.y_label);
+ });
});
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 5ece4ed080b..2c096ed08a8 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -523,6 +523,51 @@ import '~/notes';
});
});
+ describe('postComment with Slash commands', () => {
+ const sampleComment = '/assign @root\n/award :100:';
+ const note = {
+ commands_changes: {
+ assignee_id: 1,
+ emoji_award: '100'
+ },
+ errors: {
+ commands_only: ['Commands applied']
+ },
+ valid: false
+ };
+ let $form;
+ let $notesContainer;
+
+ beforeEach(() => {
+ this.notes = new Notes('', []);
+ window.gon.current_username = 'root';
+ window.gon.current_user_fullname = 'Administrator';
+ gl.awardsHandler = {
+ addAwardToEmojiBar: () => {},
+ scrollToAwards: () => {}
+ };
+ gl.GfmAutoComplete = {
+ dataSources: {
+ commands: '/root/test-project/autocomplete_sources/commands'
+ }
+ };
+ $form = $('form.js-main-target-form');
+ $notesContainer = $('ul.main-notes-list');
+ $form.find('textarea.js-note-text').val(sampleComment);
+ });
+
+ it('should remove slash command placeholder when comment with slash commands is done posting', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough();
+ $('.js-comment-button').click();
+
+ expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown
+ deferred.resolve(note);
+ expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed
+ });
+ });
+
describe('update comment with script tags', () => {
const sampleComment = '<script></script>';
const updatedComment = '<script></script>';
diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
index a4bb043f8f1..b7d82c36ddd 100644
--- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
@@ -88,12 +88,12 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
it 'queries the collection on the first call' do
expect_any_instance_of(Project).to receive(:default_issues_tracker?).once.and_call_original
- expect_any_instance_of(Project).to receive(:issue_reference_pattern).once.and_call_original
+ expect_any_instance_of(Project).to receive(:external_issue_reference_pattern).once.and_call_original
not_cached = reference_filter.call("look for #{reference}", { project: project })
expect_any_instance_of(Project).not_to receive(:default_issues_tracker?)
- expect_any_instance_of(Project).not_to receive(:issue_reference_pattern)
+ expect_any_instance_of(Project).not_to receive(:external_issue_reference_pattern)
cached = reference_filter.call("look for #{reference}", { project: project })
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index e5c1deb338b..a79d365d6c5 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -39,13 +39,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
let(:reference) { "##{issue.iid}" }
- it 'ignores valid references when using non-default tracker' do
- allow(project).to receive(:default_issues_tracker?).and_return(false)
-
- exp = act = "Issue #{reference}"
- expect(reference_filter(act).to_html).to eq exp
- end
-
it 'links to a valid reference' do
doc = reference_filter("Fixed #{reference}")
@@ -340,24 +333,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
.to eq({ project => { issue.iid => issue } })
end
end
-
- context 'using an external issue tracker' do
- it 'returns a Hash containing the issues per project' do
- doc = Nokogiri::HTML.fragment('')
- filter = described_class.new(doc, project: project)
-
- expect(project).to receive(:default_issues_tracker?).and_return(false)
-
- expect(filter).to receive(:projects_per_reference)
- .and_return({ project.path_with_namespace => project })
-
- expect(filter).to receive(:references_per_project)
- .and_return({ project.path_with_namespace => Set.new([1]) })
-
- expect(filter.issues_per_project[project][1])
- .to be_an_instance_of(ExternalIssue)
- end
- end
end
describe '.references_in' do
diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
new file mode 100644
index 00000000000..2b8c76f2bb8
--- /dev/null
+++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
@@ -0,0 +1,33 @@
+require 'rails_helper'
+
+describe Banzai::Pipeline::GfmPipeline do
+ describe 'integration between parsing regular and external issue references' do
+ let(:project) { create(:redmine_project, :public) }
+
+ it 'allows to use shorthand external reference syntax for Redmine' do
+ markdown = '#12'
+
+ result = described_class.call(markdown, project: project)[:output]
+ link = result.css('a').first
+
+ expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12'
+ end
+
+ it 'parses cross-project references to regular issues' do
+ other_project = create(:empty_project, :public)
+ issue = create(:issue, project: other_project)
+ markdown = issue.to_reference(project, full: true)
+
+ result = described_class.call(markdown, project: project)[:output]
+ link = result.css('a').first
+
+ expect(link['href']).to eq(
+ Gitlab::Routing.url_helpers.namespace_project_issue_path(
+ other_project.namespace,
+ other_project,
+ issue
+ )
+ )
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
index 58e1a0c1bc1..acdd23f81f3 100644
--- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
@@ -39,16 +39,6 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
end
-
- context 'when the project uses an external issue tracker' do
- it 'returns all nodes' do
- link = double(:link)
-
- expect(project).to receive(:external_issue_tracker).and_return(true)
-
- expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
- end
- end
end
describe '#referenced_by' do
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index af0e7855a9b..482f03aa0cc 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -598,8 +598,10 @@ module Ci
describe "Image and service handling" do
context "when extended docker configuration is used" do
it "returns image and service when defined" do
- config = YAML.dump({ image: { name: "ruby:2.1" },
- services: ["mysql", { name: "docker:dind", alias: "docker" }],
+ config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
+ services: ["mysql", { name: "docker:dind", alias: "docker",
+ entrypoint: ["/usr/local/bin/init", "run"],
+ command: ["/usr/local/bin/init", "run"] }],
before_script: ["pwd"],
rspec: { script: "rspec" } })
@@ -614,8 +616,10 @@ module Ci
coverage_regex: nil,
tag_list: [],
options: {
- image: { name: "ruby:2.1" },
- services: [{ name: "mysql" }, { name: "docker:dind", alias: "docker" }]
+ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
+ services: [{ name: "mysql" },
+ { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"],
+ command: ["/usr/local/bin/init", "run"] }]
},
allow_failure: false,
when: "on_success",
@@ -628,8 +632,11 @@ module Ci
config = YAML.dump({ image: "ruby:2.1",
services: ["mysql"],
before_script: ["pwd"],
- rspec: { image: { name: "ruby:2.5" },
- services: [{ name: "postgresql", alias: "db-pg" }, "docker:dind"], script: "rspec" } })
+ rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
+ services: [{ name: "postgresql", alias: "db-pg",
+ entrypoint: ["/usr/local/bin/init", "run"],
+ command: ["/usr/local/bin/init", "run"] }, "docker:dind"],
+ script: "rspec" } })
config_processor = GitlabCiYamlProcessor.new(config, path)
@@ -642,8 +649,10 @@ module Ci
coverage_regex: nil,
tag_list: [],
options: {
- image: { name: "ruby:2.5" },
- services: [{ name: "postgresql", alias: "db-pg" }, { name: "docker:dind" }]
+ image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
+ services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"],
+ command: ["/usr/local/bin/init", "run"] },
+ { name: "docker:dind" }]
},
allow_failure: false,
when: "on_success",
diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb
index bca22e39500..1a4d9ed5517 100644
--- a/spec/lib/gitlab/ci/config/entry/image_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb
@@ -38,7 +38,7 @@ describe Gitlab::Ci::Config::Entry::Image do
end
context 'when configuration is a hash' do
- let(:config) { { name: 'ruby:2.2', entrypoint: '/bin/sh' } }
+ let(:config) { { name: 'ruby:2.2', entrypoint: %w(/bin/sh run) } }
describe '#value' do
it 'returns image hash' do
@@ -66,7 +66,7 @@ describe Gitlab::Ci::Config::Entry::Image do
describe '#entrypoint' do
it "returns image's entrypoint" do
- expect(entry.entrypoint).to eq '/bin/sh'
+ expect(entry.entrypoint).to eq %w(/bin/sh run)
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb
index 7202fe525e4..9ebf947a751 100644
--- a/spec/lib/gitlab/ci/config/entry/service_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb
@@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::Entry::Service do
context 'when configuration is a hash' do
let(:config) do
- { name: 'postgresql:9.5', alias: 'db', command: 'cmd', entrypoint: '/bin/sh' }
+ { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run) }
end
describe '#valid?' do
@@ -72,13 +72,13 @@ describe Gitlab::Ci::Config::Entry::Service do
describe '#command' do
it "returns service's command" do
- expect(entry.command).to eq 'cmd'
+ expect(entry.command).to eq %w(cmd run)
end
end
describe '#entrypoint' do
it "returns service's entrypoint" do
- expect(entry.entrypoint).to eq '/bin/sh'
+ expect(entry.entrypoint).to eq %w(/bin/sh run)
end
end
end
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
index 8b041ac69b1..66c016d14b3 100644
--- a/spec/lib/gitlab/git/blame_spec.rb
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -20,6 +20,7 @@ describe Gitlab::Git::Blame, seed_helper: true do
expect(data.size).to eq(95)
expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
expect(data.first[:line]).to eq("# Contribute to GitLab")
+ expect(data.first[:line]).to be_utf8
end
end
@@ -40,6 +41,7 @@ describe Gitlab::Git::Blame, seed_helper: true do
expect(data.size).to eq(1)
expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
expect(data.first[:line]).to eq("Ä ü")
+ expect(data.first[:line]).to be_utf8
end
end
@@ -61,6 +63,7 @@ describe Gitlab::Git::Blame, seed_helper: true do
expect(data.size).to eq(1)
expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
expect(data.first[:line]).to eq(" ")
+ expect(data.first[:line]).to be_utf8
end
end
end
diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb
index 9dba4397e79..d1d7ed1d02a 100644
--- a/spec/lib/gitlab/git/branch_spec.rb
+++ b/spec/lib/gitlab/git/branch_spec.rb
@@ -48,7 +48,7 @@ describe Gitlab::Git::Branch, seed_helper: true do
expect(Gitlab::Git::Commit).to receive(:decorate)
.with(hash_including(attributes)).and_call_original
- expect(branch.dereferenced_target.message.encoding).to be(Encoding::UTF_8)
+ expect(branch.dereferenced_target.message).to be_utf8
end
end
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index d50ccb0df30..d97e85364c2 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -180,7 +180,7 @@ EOT
let(:raw_patch) { @raw_diff_hash[:diff].encode(Encoding::ASCII_8BIT) }
it 'encodes diff patch to UTF-8' do
- expect(diff.diff.encoding).to eq(Encoding::UTF_8)
+ expect(diff.diff).to be_utf8
end
end
end
diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb
index 3f279c21865..73518656bde 100644
--- a/spec/lib/gitlab/git/hook_spec.rb
+++ b/spec/lib/gitlab/git/hook_spec.rb
@@ -4,18 +4,20 @@ require 'fileutils'
describe Gitlab::Git::Hook, lib: true do
describe "#trigger" do
let(:project) { create(:project, :repository) }
+ let(:repo_path) { project.repository.path }
let(:user) { create(:user) }
+ let(:gl_id) { Gitlab::GlId.gl_id(user) }
def create_hook(name)
- FileUtils.mkdir_p(File.join(project.repository.path, 'hooks'))
- File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f|
+ FileUtils.mkdir_p(File.join(repo_path, 'hooks'))
+ File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f|
f.write('exit 0')
end
end
def create_failing_hook(name)
- FileUtils.mkdir_p(File.join(project.repository.path, 'hooks'))
- File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f|
+ FileUtils.mkdir_p(File.join(repo_path, 'hooks'))
+ File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f|
f.write(<<-HOOK)
echo 'regular message from the hook'
echo 'error message from the hook' 1>&2
@@ -27,13 +29,29 @@ describe Gitlab::Git::Hook, lib: true do
['pre-receive', 'post-receive', 'update'].each do |hook_name|
context "when triggering a #{hook_name} hook" do
context "when the hook is successful" do
+ let(:hook_path) { File.join(repo_path, 'hooks', hook_name) }
+ let(:gl_repository) { Gitlab::GlRepository.gl_repository(project, false) }
+ let(:env) do
+ {
+ 'GL_ID' => gl_id,
+ 'PWD' => repo_path,
+ 'GL_PROTOCOL' => 'web',
+ 'GL_REPOSITORY' => gl_repository
+ }
+ end
+
it "returns success with no errors" do
create_hook(hook_name)
- hook = Gitlab::Git::Hook.new(hook_name, project.repository.path)
+ hook = Gitlab::Git::Hook.new(hook_name, project)
blank = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch'
- status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref)
+ if hook_name != 'update'
+ expect(Open3).to receive(:popen3)
+ .with(env, hook_path, chdir: repo_path).and_call_original
+ end
+
+ status, errors = hook.trigger(gl_id, blank, blank, ref)
expect(status).to be true
expect(errors).to be_blank
end
@@ -42,11 +60,11 @@ describe Gitlab::Git::Hook, lib: true do
context "when the hook is unsuccessful" do
it "returns failure with errors" do
create_failing_hook(hook_name)
- hook = Gitlab::Git::Hook.new(hook_name, project.repository.path)
+ hook = Gitlab::Git::Hook.new(hook_name, project)
blank = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch'
- status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref)
+ status, errors = hook.trigger(gl_id, blank, blank, ref)
expect(status).to be false
expect(errors).to eq("error message from the hook\n")
end
@@ -56,11 +74,11 @@ describe Gitlab::Git::Hook, lib: true do
context "when the hook doesn't exist" do
it "returns success with no errors" do
- hook = Gitlab::Git::Hook.new('unknown_hook', project.repository.path)
+ hook = Gitlab::Git::Hook.new('unknown_hook', project)
blank = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch'
- status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref)
+ status, errors = hook.trigger(gl_id, blank, blank, ref)
expect(status).to be true
expect(errors).to be_nil
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 0cd458bf933..f1cdd86edb9 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -27,34 +27,24 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
it 'returns UTF-8' do
- expect(repository.root_ref.encoding).to eq(Encoding.find('UTF-8'))
+ expect(repository.root_ref).to be_utf8
end
- context 'with gitaly enabled' do
- before do
- stub_gitaly
- end
-
- after do
- Gitlab::GitalyClient.clear_stubs!
- end
-
- it 'gets the branch name from GitalyClient' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name)
- repository.root_ref
- end
+ it 'gets the branch name from GitalyClient' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name)
+ repository.root_ref
+ end
- it 'wraps GRPC not found' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name)
- .and_raise(GRPC::NotFound)
- expect { repository.root_ref }.to raise_error(Gitlab::Git::Repository::NoRepository)
- end
+ it 'wraps GRPC not found' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name)
+ .and_raise(GRPC::NotFound)
+ expect { repository.root_ref }.to raise_error(Gitlab::Git::Repository::NoRepository)
+ end
- it 'wraps GRPC exceptions' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name)
- .and_raise(GRPC::Unknown)
- expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError)
- end
+ it 'wraps GRPC exceptions' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name)
+ .and_raise(GRPC::Unknown)
+ expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError)
end
end
@@ -129,37 +119,27 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
it 'returns UTF-8' do
- expect(subject.first.encoding).to eq(Encoding.find('UTF-8'))
+ expect(subject.first).to be_utf8
end
it { is_expected.to include("master") }
it { is_expected.not_to include("branch-from-space") }
- context 'with gitaly enabled' do
- before do
- stub_gitaly
- end
-
- after do
- Gitlab::GitalyClient.clear_stubs!
- end
-
- it 'gets the branch names from GitalyClient' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names)
- subject
- end
+ it 'gets the branch names from GitalyClient' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names)
+ subject
+ end
- it 'wraps GRPC not found' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names)
- .and_raise(GRPC::NotFound)
- expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository)
- end
+ it 'wraps GRPC not found' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names)
+ .and_raise(GRPC::NotFound)
+ expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository)
+ end
- it 'wraps GRPC other exceptions' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names)
- .and_raise(GRPC::Unknown)
- expect { subject }.to raise_error(Gitlab::Git::CommandError)
- end
+ it 'wraps GRPC other exceptions' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names)
+ .and_raise(GRPC::Unknown)
+ expect { subject }.to raise_error(Gitlab::Git::CommandError)
end
end
@@ -173,7 +153,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
it 'returns UTF-8' do
- expect(subject.first.encoding).to eq(Encoding.find('UTF-8'))
+ expect(subject.first).to be_utf8
end
describe '#last' do
@@ -183,31 +163,21 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { is_expected.to include("v1.0.0") }
it { is_expected.not_to include("v5.0.0") }
- context 'with gitaly enabled' do
- before do
- stub_gitaly
- end
-
- after do
- Gitlab::GitalyClient.clear_stubs!
- end
-
- it 'gets the tag names from GitalyClient' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names)
- subject
- end
+ it 'gets the tag names from GitalyClient' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names)
+ subject
+ end
- it 'wraps GRPC not found' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names)
- .and_raise(GRPC::NotFound)
- expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository)
- end
+ it 'wraps GRPC not found' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names)
+ .and_raise(GRPC::NotFound)
+ expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository)
+ end
- it 'wraps GRPC exceptions' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names)
- .and_raise(GRPC::Unknown)
- expect { subject }.to raise_error(Gitlab::Git::CommandError)
- end
+ it 'wraps GRPC exceptions' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names)
+ .and_raise(GRPC::Unknown)
+ expect { subject }.to raise_error(Gitlab::Git::CommandError)
end
end
@@ -1281,42 +1251,31 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect(@repo.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true)
end
- context 'with gitaly enabled' do
- before do
- stub_gitaly
- end
-
- after do
- Gitlab::GitalyClient.clear_stubs!
- end
-
- it 'returns a Branch with UTF-8 fields' do
- branches = @repo.local_branches.to_a
- expect(branches.size).to be > 0
- utf_8 = Encoding.find('utf-8')
- branches.each do |branch|
- expect(branch.name.encoding).to eq(utf_8)
- expect(branch.target.encoding).to eq(utf_8) unless branch.target.nil?
- end
+ it 'returns a Branch with UTF-8 fields' do
+ branches = @repo.local_branches.to_a
+ expect(branches.size).to be > 0
+ branches.each do |branch|
+ expect(branch.name).to be_utf8
+ expect(branch.target).to be_utf8 unless branch.target.nil?
end
+ end
- it 'gets the branches from GitalyClient' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches)
- .and_return([])
- @repo.local_branches
- end
+ it 'gets the branches from GitalyClient' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches)
+ .and_return([])
+ @repo.local_branches
+ end
- it 'wraps GRPC not found' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches)
- .and_raise(GRPC::NotFound)
- expect { @repo.local_branches }.to raise_error(Gitlab::Git::Repository::NoRepository)
- end
+ it 'wraps GRPC not found' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches)
+ .and_raise(GRPC::NotFound)
+ expect { @repo.local_branches }.to raise_error(Gitlab::Git::Repository::NoRepository)
+ end
- it 'wraps GRPC exceptions' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches)
- .and_raise(GRPC::Unknown)
- expect { @repo.local_branches }.to raise_error(Gitlab::Git::CommandError)
- end
+ it 'wraps GRPC exceptions' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches)
+ .and_raise(GRPC::Unknown)
+ expect { @repo.local_branches }.to raise_error(Gitlab::Git::CommandError)
end
end
@@ -1395,11 +1354,4 @@ describe Gitlab::Git::Repository, seed_helper: true do
sha = Rugged::Commit.create(repo, options)
repo.lookup(sha)
end
-
- def stub_gitaly
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
-
- stub = double(:stub)
- allow(Gitaly::Ref::Stub).to receive(:new).and_return(stub)
- end
end
diff --git a/spec/lib/gitlab/gitaly_client/ref_spec.rb b/spec/lib/gitlab/gitaly_client/ref_spec.rb
index 8ad39a02b93..df22fcad902 100644
--- a/spec/lib/gitlab/gitaly_client/ref_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_spec.rb
@@ -6,17 +6,6 @@ describe Gitlab::GitalyClient::Ref do
let(:relative_path) { project.path_with_namespace + '.git' }
let(:client) { described_class.new(project.repository) }
- before do
- allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true)
- end
-
- after do
- # When we say `expect_any_instance_of(Gitaly::Ref::Stub)` a double is created,
- # and because GitalyClient shares stubs these will get passed from example to
- # example, which will cause an error, so we clean the stubs after each example.
- Gitlab::GitalyClient.clear_stubs!
- end
-
describe '#branch_names' do
it 'sends a find_all_branch_names message' do
expect_any_instance_of(Gitaly::Ref::Stub)
@@ -82,4 +71,13 @@ describe Gitlab::GitalyClient::Ref do
expect { client.local_branches(sort_by: 'invalid_sort') }.to raise_error(ArgumentError)
end
end
+
+ describe '#find_ref_name', seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
+ let(:client) { described_class.new(repository) }
+ subject { client.find_ref_name(SeedRepo::Commit::ID, 'refs/heads/master') }
+
+ it { should be_utf8 }
+ it { should eq('refs/heads/master') }
+ end
end
diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
index 61c10d47434..b333e162909 100644
--- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
@@ -97,30 +97,40 @@ describe Gitlab::HealthChecks::FsShardsCheck do
}.with_indifferent_access
end
- it { is_expected.to all(have_attributes(labels: { shard: :default })) }
+ # Unsolved intermittent failure in CI https://gitlab.com/gitlab-org/gitlab-ce/issues/31128
+ around(:each) do |example| # rubocop:disable RSpec/AroundBlock
+ times_to_try = ENV['CI'] ? 4 : 1
+ example.run_with_retry retry: times_to_try
+ end
- it { is_expected.to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) }
- it { is_expected.to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) }
- it { is_expected.to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) }
+ it 'provides metrics' do
+ expect(subject).to all(have_attributes(labels: { shard: :default }))
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0))
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_readable, value: 0))
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_writable, value: 0))
- it { is_expected.to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) }
- it { is_expected.to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) }
- it { is_expected.to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) }
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0))
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0))
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0))
+ end
end
context 'storage points to directory that has both read and write rights' do
before do
FileUtils.chmod_R(0755, tmp_dir)
end
- it { is_expected.to all(have_attributes(labels: { shard: :default })) }
- it { is_expected.to include(an_object_having_attributes(name: :filesystem_accessible, value: 1)) }
- it { is_expected.to include(an_object_having_attributes(name: :filesystem_readable, value: 1)) }
- it { is_expected.to include(an_object_having_attributes(name: :filesystem_writable, value: 1)) }
+ it 'provides metrics' do
+ expect(subject).to all(have_attributes(labels: { shard: :default }))
- it { is_expected.to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) }
- it { is_expected.to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) }
- it { is_expected.to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) }
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_accessible, value: 1))
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_readable, value: 1))
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_writable, value: 1))
+
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0))
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0))
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0))
+ end
end
end
end
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index 168a59e5139..30b6a0d8845 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -34,7 +34,7 @@ describe Gitlab::ImportExport::RepoRestorer, services: true do
it 'has the webhooks' do
restorer.restore
- expect(Gitlab::Git::Hook.new('post-receive', project.repository.path_to_repo)).to exist
+ expect(Gitlab::Git::Hook.new('post-receive', project)).to exist
end
end
end
diff --git a/spec/lib/gitlab/metrics/connection_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/connection_rack_middleware_spec.rb
new file mode 100644
index 00000000000..94251af305f
--- /dev/null
+++ b/spec/lib/gitlab/metrics/connection_rack_middleware_spec.rb
@@ -0,0 +1,88 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::ConnectionRackMiddleware do
+ let(:app) { double('app') }
+ subject { described_class.new(app) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ describe '#call' do
+ let(:status) { 100 }
+ let(:env) { { 'REQUEST_METHOD' => 'GET' } }
+ let(:stack_result) { [status, {}, 'body'] }
+
+ before do
+ allow(app).to receive(:call).and_return(stack_result)
+ end
+
+ context '@app.call succeeds with 200' do
+ before do
+ allow(app).to receive(:call).and_return([200, nil, nil])
+ end
+
+ it 'increments response count with status label' do
+ expect(described_class).to receive_message_chain(:rack_response_count, :increment).with(include(status: 200, method: 'get'))
+
+ subject.call(env)
+ end
+
+ it 'increments requests count' do
+ expect(described_class).to receive_message_chain(:rack_request_count, :increment).with(method: 'get')
+
+ subject.call(env)
+ end
+
+ it 'measures execution time' do
+ execution_time = 10
+ allow(app).to receive(:call) do |*args|
+ Timecop.freeze(execution_time.seconds)
+ end
+
+ expect(described_class).to receive_message_chain(:rack_execution_time, :observe).with({}, execution_time)
+
+ subject.call(env)
+ end
+ end
+
+ context '@app.call throws exception' do
+ let(:rack_response_count) { double('rack_response_count') }
+
+ before do
+ allow(app).to receive(:call).and_raise(StandardError)
+ allow(described_class).to receive(:rack_response_count).and_return(rack_response_count)
+ end
+
+ it 'increments exceptions count' do
+ expect(described_class).to receive_message_chain(:rack_uncaught_errors_count, :increment)
+
+ expect { subject.call(env) }.to raise_error(StandardError)
+ end
+
+ it 'increments requests count' do
+ expect(described_class).to receive_message_chain(:rack_request_count, :increment).with(method: 'get')
+
+ expect { subject.call(env) }.to raise_error(StandardError)
+ end
+
+ it "does't increment response count" do
+ expect(described_class.rack_response_count).not_to receive(:increment)
+
+ expect { subject.call(env) }.to raise_error(StandardError)
+ end
+
+ it 'measures execution time' do
+ execution_time = 10
+ allow(app).to receive(:call) do |*args|
+ Timecop.freeze(execution_time.seconds)
+ raise StandardError
+ end
+
+ expect(described_class).to receive_message_chain(:rack_execution_time, :observe).with({}, execution_time)
+
+ expect { subject.call(env) }.to raise_error(StandardError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/sampler_spec.rb b/spec/lib/gitlab/metrics/influx_sampler_spec.rb
index d07ce6f81af..0bc68d64276 100644
--- a/spec/lib/gitlab/metrics/sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/influx_sampler_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Metrics::Sampler do
+describe Gitlab::Metrics::InfluxSampler do
let(:sampler) { described_class.new(5) }
after do
@@ -8,10 +8,10 @@ describe Gitlab::Metrics::Sampler do
end
describe '#start' do
- it 'gathers a sample at a given interval' do
- expect(sampler).to receive(:sleep).with(a_kind_of(Numeric))
- expect(sampler).to receive(:sample)
- expect(sampler).to receive(:loop).and_yield
+ it 'runs once and gathers a sample at a given interval' do
+ expect(sampler).to receive(:sleep).with(a_kind_of(Numeric)).twice
+ expect(sampler).to receive(:sample).once
+ expect(sampler).to receive(:running).and_return(false, true, false)
sampler.start.join
end
diff --git a/spec/lib/gitlab/metrics/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/unicorn_sampler_spec.rb
new file mode 100644
index 00000000000..dc0d1f2e940
--- /dev/null
+++ b/spec/lib/gitlab/metrics/unicorn_sampler_spec.rb
@@ -0,0 +1,108 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::UnicornSampler do
+ subject { described_class.new(1.second) }
+
+ describe '#sample' do
+ let(:unicorn) { double('unicorn') }
+ let(:raindrops) { double('raindrops') }
+ let(:stats) { double('stats') }
+
+ before do
+ stub_const('Unicorn', unicorn)
+ stub_const('Raindrops::Linux', raindrops)
+ allow(raindrops).to receive(:unix_listener_stats).and_return({})
+ allow(raindrops).to receive(:tcp_listener_stats).and_return({})
+ end
+
+ context 'unicorn listens on unix sockets' do
+ let(:socket_address) { '/some/sock' }
+ let(:sockets) { [socket_address] }
+
+ before do
+ allow(unicorn).to receive(:listener_names).and_return(sockets)
+ end
+
+ it 'samples socket data' do
+ expect(raindrops).to receive(:unix_listener_stats).with(sockets)
+
+ subject.sample
+ end
+
+ context 'stats collected' do
+ before do
+ allow(stats).to receive(:active).and_return('active')
+ allow(stats).to receive(:queued).and_return('queued')
+ allow(raindrops).to receive(:unix_listener_stats).and_return({ socket_address => stats })
+ end
+
+ it 'updates metrics type unix and with addr' do
+ labels = { type: 'unix', address: socket_address }
+
+ expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
+ expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
+
+ subject.sample
+ end
+ end
+ end
+
+ context 'unicorn listens on tcp sockets' do
+ let(:tcp_socket_address) { '0.0.0.0:8080' }
+ let(:tcp_sockets) { [tcp_socket_address] }
+
+ before do
+ allow(unicorn).to receive(:listener_names).and_return(tcp_sockets)
+ end
+
+ it 'samples socket data' do
+ expect(raindrops).to receive(:tcp_listener_stats).with(tcp_sockets)
+
+ subject.sample
+ end
+
+ context 'stats collected' do
+ before do
+ allow(stats).to receive(:active).and_return('active')
+ allow(stats).to receive(:queued).and_return('queued')
+ allow(raindrops).to receive(:tcp_listener_stats).and_return({ tcp_socket_address => stats })
+ end
+
+ it 'updates metrics type unix and with addr' do
+ labels = { type: 'tcp', address: tcp_socket_address }
+
+ expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
+ expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
+
+ subject.sample
+ end
+ end
+ end
+ end
+
+ describe '#start' do
+ context 'when enabled' do
+ before do
+ allow(subject).to receive(:enabled?).and_return(true)
+ end
+
+ it 'creates new thread' do
+ expect(Thread).to receive(:new)
+
+ subject.start
+ end
+ end
+
+ context 'when disabled' do
+ before do
+ allow(subject).to receive(:enabled?).and_return(false)
+ end
+
+ it "doesn't create new thread" do
+ expect(Thread).not_to receive(:new)
+
+ subject.start
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb
index 4ae216d55b0..af50ecdb2ab 100644
--- a/spec/lib/gitlab/popen_spec.rb
+++ b/spec/lib/gitlab/popen_spec.rb
@@ -32,6 +32,17 @@ describe 'Gitlab::Popen', lib: true, no_db: true do
end
end
+ context 'with custom options' do
+ let(:vars) { { 'foobar' => 123, 'PWD' => path } }
+ let(:options) { { chdir: path } }
+
+ it 'calls popen3 with the provided environment variables' do
+ expect(Open3).to receive(:popen3).with(vars, 'ls', options)
+
+ @output, @status = @klass.new.popen(%w(ls), path, { 'foobar' => 123 })
+ end
+ end
+
context 'without a directory argument' do
before do
@output, @status = @klass.new.popen(%w(ls))
@@ -45,7 +56,7 @@ describe 'Gitlab::Popen', lib: true, no_db: true do
before do
@output, @status = @klass.new.popen(%w[cat]) { |stdin| stdin.write 'hello' }
end
-
+
it { expect(@status).to be_zero }
it { expect(@output).to eq('hello') }
end
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index a97a0f8452b..5b1b8f9516a 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -4,6 +4,7 @@ require 'stringio'
describe Gitlab::Shell, lib: true do
let(:project) { double('Project', id: 7, path: 'diaspora') }
let(:gitlab_shell) { Gitlab::Shell.new }
+ let(:popen_vars) { { 'GIT_TERMINAL_PROMPT' => ENV['GIT_TERMINAL_PROMPT'] } }
before do
allow(Project).to receive(:find).and_return(project)
@@ -50,7 +51,7 @@ describe Gitlab::Shell, lib: true do
describe '#add_key' do
it 'removes trailing garbage' do
allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
- expect(Gitlab::Utils).to receive(:system_silent).with(
+ expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with(
[:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar']
)
@@ -100,17 +101,91 @@ describe Gitlab::Shell, lib: true do
allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800)
end
+ describe '#add_repository' do
+ it 'returns true when the command succeeds' do
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([projects_path, 'add-project', 'current/storage', 'project/path.git'],
+ nil, popen_vars).and_return([nil, 0])
+
+ expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be true
+ end
+
+ it 'returns false when the command fails' do
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([projects_path, 'add-project', 'current/storage', 'project/path.git'],
+ nil, popen_vars).and_return(["error", 1])
+
+ expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be false
+ end
+ end
+
+ describe '#remove_repository' do
+ it 'returns true when the command succeeds' do
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([projects_path, 'rm-project', 'current/storage', 'project/path.git'],
+ nil, popen_vars).and_return([nil, 0])
+
+ expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be true
+ end
+
+ it 'returns false when the command fails' do
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([projects_path, 'rm-project', 'current/storage', 'project/path.git'],
+ nil, popen_vars).and_return(["error", 1])
+
+ expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be false
+ end
+ end
+
+ describe '#mv_repository' do
+ it 'returns true when the command succeeds' do
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([projects_path, 'mv-project', 'current/storage', 'project/path.git', 'project/newpath.git'],
+ nil, popen_vars).and_return([nil, 0])
+
+ expect(gitlab_shell.mv_repository('current/storage', 'project/path', 'project/newpath')).to be true
+ end
+
+ it 'returns false when the command fails' do
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([projects_path, 'mv-project', 'current/storage', 'project/path.git', 'project/newpath.git'],
+ nil, popen_vars).and_return(["error", 1])
+
+ expect(gitlab_shell.mv_repository('current/storage', 'project/path', 'project/newpath')).to be false
+ end
+ end
+
+ describe '#fork_repository' do
+ it 'returns true when the command succeeds' do
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([projects_path, 'fork-project', 'current/storage', 'project/path.git', 'new/storage', 'new-namespace'],
+ nil, popen_vars).and_return([nil, 0])
+
+ expect(gitlab_shell.fork_repository('current/storage', 'project/path', 'new/storage', 'new-namespace')).to be true
+ end
+
+ it 'return false when the command fails' do
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([projects_path, 'fork-project', 'current/storage', 'project/path.git', 'new/storage', 'new-namespace'],
+ nil, popen_vars).and_return(["error", 1])
+
+ expect(gitlab_shell.fork_repository('current/storage', 'project/path', 'new/storage', 'new-namespace')).to be false
+ end
+ end
+
describe '#fetch_remote' do
it 'returns true when the command succeeds' do
expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'fetch-remote', 'current/storage', 'project/path.git', 'new/storage', '800']).and_return([nil, 0])
+ .with([projects_path, 'fetch-remote', 'current/storage', 'project/path.git', 'new/storage', '800'],
+ nil, popen_vars).and_return([nil, 0])
expect(gitlab_shell.fetch_remote('current/storage', 'project/path', 'new/storage')).to be true
end
it 'raises an exception when the command fails' do
expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'fetch-remote', 'current/storage', 'project/path.git', 'new/storage', '800']).and_return(["error", 1])
+ .with([projects_path, 'fetch-remote', 'current/storage', 'project/path.git', 'new/storage', '800'],
+ nil, popen_vars).and_return(["error", 1])
expect { gitlab_shell.fetch_remote('current/storage', 'project/path', 'new/storage') }.to raise_error(Gitlab::Shell::Error, "error")
end
@@ -119,14 +194,16 @@ describe Gitlab::Shell, lib: true do
describe '#import_repository' do
it 'returns true when the command succeeds' do
expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"]).and_return([nil, 0])
+ .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"],
+ nil, popen_vars).and_return([nil, 0])
expect(gitlab_shell.import_repository('current/storage', 'project/path', 'https://gitlab.com/gitlab-org/gitlab-ce.git')).to be true
end
it 'raises an exception when the command fails' do
expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"]).and_return(["error", 1])
+ .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"],
+ nil, popen_vars).and_return(["error", 1])
expect { gitlab_shell.import_repository('current/storage', 'project/path', 'https://gitlab.com/gitlab-org/gitlab-ce.git') }.to raise_error(Gitlab::Shell::Error, "error")
end
diff --git a/spec/migrations/rename_duplicated_variable_key_spec.rb b/spec/migrations/rename_duplicated_variable_key_spec.rb
new file mode 100644
index 00000000000..11096564dfa
--- /dev/null
+++ b/spec/migrations/rename_duplicated_variable_key_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20170622135451_rename_duplicated_variable_key.rb')
+
+describe RenameDuplicatedVariableKey, :migration do
+ let(:variables) { table(:ci_variables) }
+ let(:projects) { table(:projects) }
+
+ before do
+ projects.create!(id: 1)
+ variables.create!(id: 1, key: 'key1', project_id: 1)
+ variables.create!(id: 2, key: 'key2', project_id: 1)
+ variables.create!(id: 3, key: 'keyX', project_id: 1)
+ variables.create!(id: 4, key: 'keyX', project_id: 1)
+ variables.create!(id: 5, key: 'keyY', project_id: 1)
+ variables.create!(id: 6, key: 'keyX', project_id: 1)
+ variables.create!(id: 7, key: 'key7', project_id: 1)
+ variables.create!(id: 8, key: 'keyY', project_id: 1)
+ end
+
+ it 'correctly remove duplicated records with smaller id' do
+ migrate!
+
+ expect(variables.pluck(:id, :key)).to contain_exactly(
+ [1, 'key1'],
+ [2, 'key2'],
+ [3, 'keyX_3'],
+ [4, 'keyX_4'],
+ [5, 'keyY_5'],
+ [6, 'keyX'],
+ [7, 'key7'],
+ [8, 'keyY']
+ )
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 488697f74eb..7de5e2e3920 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -998,13 +998,17 @@ describe Ci::Build, :models do
describe '#ref_slug' do
{
- 'master' => 'master',
- '1-foo' => '1-foo',
- 'fix/1-foo' => 'fix-1-foo',
- 'fix-1-foo' => 'fix-1-foo',
- 'a' * 63 => 'a' * 63,
- 'a' * 64 => 'a' * 63,
- 'FOO' => 'foo'
+ 'master' => 'master',
+ '1-foo' => '1-foo',
+ 'fix/1-foo' => 'fix-1-foo',
+ 'fix-1-foo' => 'fix-1-foo',
+ 'a' * 63 => 'a' * 63,
+ 'a' * 64 => 'a' * 63,
+ 'FOO' => 'foo',
+ '-' + 'a' * 61 + '-' => 'a' * 61,
+ '-' + 'a' * 62 + '-' => 'a' * 62,
+ '-' + 'a' * 63 + '-' => 'a' * 62,
+ 'a' * 62 + ' ' => 'a' * 62
}.each do |ref, slug|
it "transforms #{ref} to #{slug}" do
build.ref = ref
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 329682a0771..50f7c029af8 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -3,8 +3,16 @@ require 'spec_helper'
describe Ci::Variable, models: true do
subject { build(:ci_variable) }
- it { is_expected.to include_module(HasVariable) }
- it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id) }
+ let(:secret_value) { 'secret' }
+
+ describe 'validations' do
+ it { is_expected.to include_module(HasVariable) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope) }
+ it { is_expected.to validate_length_of(:key).is_at_most(255) }
+ it { is_expected.to allow_value('foo').for(:key) }
+ it { is_expected.not_to allow_value('foo bar').for(:key) }
+ it { is_expected.not_to allow_value('foo/bar').for(:key) }
+ end
describe '.unprotected' do
subject { described_class.unprotected }
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index 65f05121b40..36aedd2f701 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -132,6 +132,19 @@ describe Group, 'Routable' do
end
end
+ describe '#expires_full_path_cache' do
+ context 'with RequestStore active', :request_store do
+ it 'expires the full_path cache' do
+ expect(group.full_path).to eq('foo')
+
+ group.route.update(path: 'bar', name: 'bar')
+ group.expires_full_path_cache
+
+ expect(group.full_path).to eq('bar')
+ end
+ end
+ end
+
describe '#full_name' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index b0635c6a90a..0a2cd8c2957 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -120,28 +120,17 @@ describe Environment, models: true do
let(:head_commit) { project.commit }
let(:commit) { project.commit.parent }
- context 'Gitaly find_ref_name feature disabled' do
- it 'returns deployment id for the environment' do
- expect(environment.first_deployment_for(commit)).to eq deployment1
- end
+ it 'returns deployment id for the environment' do
+ expect(environment.first_deployment_for(commit)).to eq deployment1
+ end
- it 'return nil when no deployment is found' do
- expect(environment.first_deployment_for(head_commit)).to eq nil
- end
+ it 'return nil when no deployment is found' do
+ expect(environment.first_deployment_for(head_commit)).to eq nil
end
- # TODO: Uncomment when feature is reenabled
- # context 'Gitaly find_ref_name feature enabled' do
- # before do
- # allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:find_ref_name).and_return(true)
- # end
- #
- # it 'calls GitalyClient' do
- # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:find_ref_name)
- #
- # environment.first_deployment_for(commit)
- # end
- # end
+ it 'returns a UTF-8 ref' do
+ expect(environment.first_deployment_for(commit).ref).to be_utf8
+ end
end
describe '#environment_type' do
diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
index 6e8d43f988c..5c13cf584f9 100644
--- a/spec/models/forked_project_link_spec.rb
+++ b/spec/models/forked_project_link_spec.rb
@@ -2,53 +2,75 @@ require 'spec_helper'
describe ForkedProjectLink, "add link on fork" do
let(:project_from) { create(:project, :repository) }
+ let(:project_to) { fork_project(project_from, user) }
let(:user) { create(:user) }
let(:namespace) { user.namespace }
before do
- create(:project_member, :reporter, user: user, project: project_from)
- @project_to = fork_project(project_from, user)
+ project_from.add_reporter(user)
+ end
+
+ it 'project_from knows its forks' do
+ _ = project_to
+
+ expect(project_from.forks.count).to eq(1)
end
it "project_to knows it is forked" do
- expect(@project_to.forked?).to be_truthy
+ expect(project_to.forked?).to be_truthy
end
it "project knows who it is forked from" do
- expect(@project_to.forked_from_project).to eq(project_from)
+ expect(project_to.forked_from_project).to eq(project_from)
end
-end
-describe '#forked?' do
- let(:forked_project_link) { build(:forked_project_link) }
- let(:project_from) { create(:project, :repository) }
- let(:project_to) { create(:project, forked_project_link: forked_project_link) }
+ context 'project_to is pending_delete' do
+ before do
+ project_to.update!(pending_delete: true)
+ end
- before :each do
- forked_project_link.forked_from_project = project_from
- forked_project_link.forked_to_project = project_to
- forked_project_link.save!
+ it { expect(project_from.forks.count).to eq(0) }
end
- it "project_to knows it is forked" do
- expect(project_to.forked?).to be_truthy
- end
+ context 'project_from is pending_delete' do
+ before do
+ project_from.update!(pending_delete: true)
+ end
- it "project_from is not forked" do
- expect(project_from.forked?).to be_falsey
+ it { expect(project_to.forked_from_project).to be_nil }
end
- it "project_to.destroy destroys fork_link" do
- expect(forked_project_link).to receive(:destroy)
- project_to.destroy
+ describe '#forked?' do
+ let(:project_to) { create(:project, forked_project_link: forked_project_link) }
+ let(:forked_project_link) { build(:forked_project_link) }
+
+ before do
+ forked_project_link.forked_from_project = project_from
+ forked_project_link.forked_to_project = project_to
+ forked_project_link.save!
+ end
+
+ it "project_to knows it is forked" do
+ expect(project_to.forked?).to be_truthy
+ end
+
+ it "project_from is not forked" do
+ expect(project_from.forked?).to be_falsey
+ end
+
+ it "project_to.destroy destroys fork_link" do
+ expect(forked_project_link).to receive(:destroy)
+
+ project_to.destroy
+ end
end
-end
-def fork_project(from_project, user)
- shell = double('gitlab_shell', fork_repository: true)
+ def fork_project(from_project, user)
+ service = Projects::ForkService.new(from_project, user)
+ shell = double('gitlab_shell', fork_repository: true)
- service = Projects::ForkService.new(from_project, user)
- allow(service).to receive(:gitlab_shell).and_return(shell)
+ allow(service).to receive(:gitlab_shell).and_return(shell)
- service.execute
+ service.execute
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index bb5273074a2..587d4b83cb4 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -105,6 +105,22 @@ describe MergeRequest, models: true do
end
end
+ describe '#assignee_ids' do
+ it 'returns an array of the assigned user id' do
+ subject.assignee_id = 123
+
+ expect(subject.assignee_ids).to eq([123])
+ end
+ end
+
+ describe '#assignee_ids=' do
+ it 'sets assignee_id to the last id in the array' do
+ subject.assignee_ids = [123, 456]
+
+ expect(subject.assignee_id).to eq(456)
+ end
+ end
+
describe '#assignee_or_author?' do
let(:user) { create(:user) }
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index c86f56c55eb..4a1de76f099 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -64,12 +64,12 @@ describe JiraService, models: true do
end
end
- describe '#reference_pattern' do
+ describe '.reference_pattern' do
it_behaves_like 'allows project key on reference pattern'
it 'does not allow # on the code' do
- expect(subject.reference_pattern.match('#123')).to be_nil
- expect(subject.reference_pattern.match('1#23#12')).to be_nil
+ expect(described_class.reference_pattern.match('#123')).to be_nil
+ expect(described_class.reference_pattern.match('1#23#12')).to be_nil
end
end
diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb
index 6631d9040b1..441b3f896ca 100644
--- a/spec/models/project_services/redmine_service_spec.rb
+++ b/spec/models/project_services/redmine_service_spec.rb
@@ -31,11 +31,11 @@ describe RedmineService, models: true do
end
end
- describe '#reference_pattern' do
+ describe '.reference_pattern' do
it_behaves_like 'allows project key on reference pattern'
it 'does allow # on the reference' do
- expect(subject.reference_pattern.match('#123')[:issue]).to eq('123')
+ expect(described_class.reference_pattern.match('#123')[:issue]).to eq('123')
end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 1390848ff4a..ad98b4b669f 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1216,6 +1216,8 @@ describe Project, models: true do
expect(project).to receive(:expire_caches_before_rename)
+ expect(project).to receive(:expires_full_path_cache)
+
project.rename_repo
end
@@ -1344,7 +1346,7 @@ describe Project, models: true do
.with(project.repository_storage_path, project.path_with_namespace)
.and_return(true)
- expect(project).to receive(:create_repository)
+ expect(project).to receive(:create_repository).with(force: true)
project.ensure_repository
end
@@ -1357,6 +1359,19 @@ describe Project, models: true do
project.ensure_repository
end
+
+ it 'creates the repository if it is a fork' do
+ expect(project).to receive(:forked?).and_return(true)
+
+ allow(project).to receive(:repository_exists?)
+ .and_return(false)
+
+ expect(shell).to receive(:add_repository)
+ .with(project.repository_storage_path, project.path_with_namespace)
+ .and_return(true)
+
+ project.ensure_repository
+ end
end
describe '#user_can_push_to_empty_repo?' do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 3e984ec7588..c69f0a495db 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -780,7 +780,7 @@ describe Repository, models: true do
context 'when pre hooks were successful' do
it 'runs without errors' do
expect_any_instance_of(GitHooksService).to receive(:execute)
- .with(user, project.repository.path_to_repo, old_rev, blank_sha, 'refs/heads/feature')
+ .with(user, project, old_rev, blank_sha, 'refs/heads/feature')
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
end
@@ -823,12 +823,7 @@ describe Repository, models: true do
service = GitHooksService.new
expect(GitHooksService).to receive(:new).and_return(service)
expect(service).to receive(:execute)
- .with(
- user,
- repository.path_to_repo,
- old_rev,
- new_rev,
- 'refs/heads/feature')
+ .with(user, project, old_rev, new_rev, 'refs/heads/feature')
.and_yield(service).and_return(true)
end
@@ -1474,9 +1469,9 @@ describe Repository, models: true do
it 'passes commit SHA to pre-receive and update hooks,\
and tag SHA to post-receive hook' do
- pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', repository.path_to_repo)
- update_hook = Gitlab::Git::Hook.new('update', repository.path_to_repo)
- post_receive_hook = Gitlab::Git::Hook.new('post-receive', repository.path_to_repo)
+ pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', project)
+ update_hook = Gitlab::Git::Hook.new('update', project)
+ post_receive_hook = Gitlab::Git::Hook.new('post-receive', project)
allow(Gitlab::Git::Hook).to receive(:new)
.and_return(pre_receive_hook, update_hook, post_receive_hook)
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
new file mode 100644
index 00000000000..bb0fa0c0e9c
--- /dev/null
+++ b/spec/policies/global_policy_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe GlobalPolicy, models: true do
+ let(:current_user) { create(:user) }
+ let(:user) { create(:user) }
+
+ subject { GlobalPolicy.new(current_user, [user]) }
+
+ describe "reading the list of users" do
+ context "for a logged in user" do
+ it { is_expected.to be_allowed(:read_users_list) }
+ end
+
+ context "for an anonymous user" do
+ let(:current_user) { nil }
+
+ context "when the public level is restricted" do
+ before do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+ end
+
+ it { is_expected.not_to be_allowed(:read_users_list) }
+ end
+
+ context "when the public level is not restricted" do
+ before do
+ stub_application_setting(restricted_visibility_levels: [])
+ end
+
+ it { is_expected.to be_allowed(:read_users_list) }
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index c0174b304c8..8640c16203e 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -13,9 +13,40 @@ describe API::Users do
describe 'GET /users' do
context "when unauthenticated" do
- it "returns authentication error" do
+ it "returns authorization error when the `username` parameter is not passed" do
get api("/users")
- expect(response).to have_http_status(401)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "returns the user when a valid `username` parameter is passed" do
+ user = create(:user)
+
+ get api("/users"), username: user.username
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response[0]['id']).to eq(user.id)
+ expect(json_response[0]['username']).to eq(user.username)
+ end
+
+ it "returns authorization error when the `username` parameter refers to an inaccessible user" do
+ user = create(:user)
+
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+
+ get api("/users"), username: user.username
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "returns an empty response when an invalid `username` parameter is passed" do
+ get api("/users"), username: 'invalid'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(0)
end
end
@@ -138,6 +169,7 @@ describe API::Users do
describe "GET /users/:id" do
it "returns a user by id" do
get api("/users/#{user.id}", user)
+
expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user.username)
end
@@ -148,9 +180,22 @@ describe API::Users do
expect(json_response['is_admin']).to be_nil
end
- it "returns a 401 if unauthenticated" do
- get api("/users/9998")
- expect(response).to have_http_status(401)
+ context 'for an anonymous user' do
+ it "returns a user by id" do
+ get api("/users/#{user.id}")
+
+ expect(response).to have_http_status(200)
+ expect(json_response['username']).to eq(user.username)
+ end
+
+ it "returns a 404 if the target user is present but inaccessible" do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(nil, :read_user, user).and_return(false)
+
+ get api("/users/#{user.id}")
+
+ expect(response).to have_http_status(404)
+ end
end
it "returns a 404 error if user id not found" do
diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb
index cae74df9c90..fe21ca0b3cb 100644
--- a/spec/services/delete_merged_branches_service_spec.rb
+++ b/spec/services/delete_merged_branches_service_spec.rb
@@ -24,6 +24,14 @@ describe DeleteMergedBranchesService, services: true do
expect(project.repository.branch_names).to include('master')
end
+ it 'keeps protected branches' do
+ create(:protected_branch, project: project, name: 'improve/awesome')
+
+ service.execute
+
+ expect(project.repository.branch_names).to include('improve/awesome')
+ end
+
context 'user without rights' do
let(:user) { create(:user) }
diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb
index ac7ccfbaab0..213678c27f5 100644
--- a/spec/services/git_hooks_service_spec.rb
+++ b/spec/services/git_hooks_service_spec.rb
@@ -12,7 +12,6 @@ describe GitHooksService, services: true do
@oldrev = sample_commit.parent_id
@newrev = sample_commit.id
@ref = 'refs/heads/feature'
- @repo_path = project.repository.path_to_repo
end
describe '#execute' do
@@ -21,7 +20,7 @@ describe GitHooksService, services: true do
hook = double(trigger: [true, nil])
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
- service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }
+ service.execute(user, project, @blankrev, @newrev, @ref) { }
end
end
@@ -31,7 +30,7 @@ describe GitHooksService, services: true do
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
- service.execute(user, @repo_path, @blankrev, @newrev, @ref)
+ service.execute(user, project, @blankrev, @newrev, @ref)
end.to raise_error(GitHooksService::PreReceiveError)
end
end
@@ -43,7 +42,7 @@ describe GitHooksService, services: true do
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
- service.execute(user, @repo_path, @blankrev, @newrev, @ref)
+ service.execute(user, project, @blankrev, @newrev, @ref)
end.to raise_error(GitHooksService::PreReceiveError)
end
end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index ca827fc0f39..8e8816870e1 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -401,18 +401,6 @@ describe GitPushService, services: true do
expect(SystemNoteService).not_to receive(:cross_reference)
execute_service(project, commit_author, @oldrev, @newrev, @ref )
end
-
- it "doesn't close issues when external issue tracker is in use" do
- allow_any_instance_of(Project).to receive(:default_issues_tracker?)
- .and_return(false)
- external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid, reference_pattern: project.issue_reference_pattern)
- allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(external_issue_tracker)
-
- # The push still shouldn't create cross-reference notes.
- expect do
- execute_service(project, commit_author, @oldrev, @newrev, 'refs/heads/hurf' )
- end.not_to change { Note.where(project_id: project.id, system: true).count }
- end
end
context "to non-default branches" do
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 76c52d55ae5..441a5276c56 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -30,6 +30,12 @@ describe Projects::TransferService, services: true do
transfer_project(project, user, group)
end
+ it 'expires full_path cache' do
+ expect(project).to receive(:expires_full_path_cache)
+
+ transfer_project(project, user, group)
+ end
+
it 'executes system hooks' do
expect_any_instance_of(Projects::TransferService).to receive(:execute_system_hooks)
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index c9e63efbc14..35373675894 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -359,18 +359,18 @@ describe QuickActions::InterpretService, services: true do
let(:content) { "/assign @#{developer.username}" }
context 'Issue' do
- it 'fetches assignee and populates assignee_id if content contains /assign' do
+ it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, issue)
- expect(updates).to eq(assignee_ids: [developer.id])
+ expect(updates[:assignee_ids]).to match_array([developer.id])
end
end
context 'Merge Request' do
- it 'fetches assignee and populates assignee_id if content contains /assign' do
+ it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, merge_request)
- expect(updates).to eq(assignee_id: developer.id)
+ expect(updates).to eq(assignee_ids: [developer.id])
end
end
end
@@ -383,7 +383,7 @@ describe QuickActions::InterpretService, services: true do
end
context 'Issue' do
- it 'fetches assignee and populates assignee_id if content contains /assign' do
+ it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, issue)
expect(updates[:assignee_ids]).to match_array([developer.id])
@@ -391,10 +391,10 @@ describe QuickActions::InterpretService, services: true do
end
context 'Merge Request' do
- it 'fetches assignee and populates assignee_id if content contains /assign' do
+ it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, merge_request)
- expect(updates).to eq(assignee_id: developer.id)
+ expect(updates).to eq(assignee_ids: [developer.id])
end
end
end
@@ -422,11 +422,27 @@ describe QuickActions::InterpretService, services: true do
end
context 'Merge Request' do
- it 'populates assignee_id: nil if content contains /unassign' do
- merge_request.update(assignee_id: developer.id)
+ it 'populates assignee_ids: [] if content contains /unassign' do
+ merge_request.update(assignee_ids: [developer.id])
_, updates = service.execute(content, merge_request)
- expect(updates).to eq(assignee_id: nil)
+ expect(updates).to eq(assignee_ids: [])
+ end
+ end
+ end
+
+ context 'reassign command' do
+ let(:content) { '/reassign' }
+
+ context 'Issue' do
+ it 'reassigns user if content contains /reassign @user' do
+ user = create(:user)
+
+ issue.update(assignee_ids: [developer.id])
+
+ _, updates = service.execute("/reassign @#{user.username}", issue)
+
+ expect(updates).to eq(assignee_ids: [user.id])
end
end
end
diff --git a/spec/support/issue_tracker_service_shared_example.rb b/spec/support/issue_tracker_service_shared_example.rb
index e70b3963d9d..a6ab03cb808 100644
--- a/spec/support/issue_tracker_service_shared_example.rb
+++ b/spec/support/issue_tracker_service_shared_example.rb
@@ -8,15 +8,15 @@ end
RSpec.shared_examples 'allows project key on reference pattern' do |url_attr|
it 'allows underscores in the project name' do
- expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
+ expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
end
it 'allows numbers in the project name' do
- expect(subject.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234'
+ expect(described_class.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234'
end
it 'requires the project name to begin with A-Z' do
- expect(subject.reference_pattern.match('3EXT_EXT-1234')).to eq nil
- expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
+ expect(described_class.reference_pattern.match('3EXT_EXT-1234')).to eq nil
+ expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
end
end
diff --git a/spec/support/matchers/be_utf8.rb b/spec/support/matchers/be_utf8.rb
new file mode 100644
index 00000000000..ea806352422
--- /dev/null
+++ b/spec/support/matchers/be_utf8.rb
@@ -0,0 +1,9 @@
+RSpec::Matchers.define :be_utf8 do |_|
+ match do |actual|
+ actual.is_a?(String) && actual.encoding == Encoding.find('UTF-8')
+ end
+
+ description do
+ "be a String with encoding UTF-8"
+ end
+end
diff --git a/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb b/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb
new file mode 100644
index 00000000000..96c821b26f7
--- /dev/null
+++ b/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb
@@ -0,0 +1,9 @@
+shared_examples 'issue sidebar stays collapsed on mobile' do
+ before do
+ resize_screen_xs
+ end
+
+ it 'keeps the sidebar collapsed' do
+ expect(page).not_to have_css('.right-sidebar.right-sidebar-collapsed')
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index b902d5235d0..b04eebe60af 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -265,6 +265,15 @@ babel-core@^6.22.1, babel-core@^6.23.0:
slash "^1.0.0"
source-map "^0.5.0"
+babel-eslint@^7.2.1:
+ version "7.2.1"
+ resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.1.tgz#079422eb73ba811e3ca0865ce87af29327f8c52f"
+ dependencies:
+ babel-code-frame "^6.22.0"
+ babel-traverse "^6.23.1"
+ babel-types "^6.23.0"
+ babylon "^6.16.1"
+
babel-generator@^6.18.0, babel-generator@^6.23.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.23.0.tgz#6b8edab956ef3116f79d8c84c5a3c05f32a74bc5"
@@ -816,10 +825,14 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23
lodash "^4.2.0"
to-fast-properties "^1.0.1"
-babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0:
+babylon@^6.11.0:
version "6.15.0"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e"
+babylon@^6.13.0, babylon@^6.15.0, babylon@^6.16.1:
+ version "6.16.1"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3"
+
backo2@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"