summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml5
-rw-r--r--CONTRIBUTING.md5
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/branches/branches_delete_modal.js8
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js2
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js5
-rw-r--r--app/assets/javascripts/fly_out_nav.js3
-rw-r--r--app/assets/javascripts/labels_select.js7
-rw-r--r--app/assets/javascripts/lib/utils/animate.js49
-rw-r--r--app/assets/javascripts/main.js1
-rw-r--r--app/assets/javascripts/milestone_select.js3
-rw-r--r--app/assets/javascripts/new_sidebar.js5
-rw-r--r--app/assets/javascripts/notes/components/issue_note_actions.vue2
-rw-r--r--app/assets/javascripts/shortcuts.js1
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss20
-rw-r--r--app/assets/stylesheets/framework/nav.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/new_sidebar.scss29
-rw-r--r--app/assets/stylesheets/pages/builds.scss8
-rw-r--r--app/assets/stylesheets/pages/diff.scss10
-rw-r--r--app/assets/stylesheets/pages/issuable.scss6
-rw-r--r--app/assets/stylesheets/pages/login.scss2
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss5
-rw-r--r--app/assets/stylesheets/pages/projects.scss2
-rw-r--r--app/assets/stylesheets/pages/repo.scss14
-rw-r--r--app/controllers/admin/deploy_keys_controller.rb5
-rw-r--r--app/controllers/admin/labels_controller.rb2
-rw-r--r--app/controllers/autocomplete_controller.rb47
-rw-r--r--app/controllers/boards/issues_controller.rb13
-rw-r--r--app/controllers/profiles/gpg_keys_controller.rb4
-rw-r--r--app/controllers/profiles/keys_controller.rb4
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb2
-rw-r--r--app/controllers/projects/compare_controller.rb2
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb4
-rw-r--r--app/finders/autocomplete_users_finder.rb60
-rw-r--r--app/helpers/boards_helper.rb4
-rw-r--r--app/helpers/groups_helper.rb6
-rw-r--r--app/helpers/tab_helper.rb4
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/deploy_key.rb6
-rw-r--r--app/models/environment.rb5
-rw-r--r--app/models/gpg_key.rb5
-rw-r--r--app/models/key.rb5
-rw-r--r--app/models/label.rb7
-rw-r--r--app/models/namespace.rb9
-rw-r--r--app/models/personal_access_token.rb2
-rw-r--r--app/models/project.rb21
-rw-r--r--app/models/project_feature.rb2
-rw-r--r--app/models/project_services/pipelines_email_service.rb2
-rw-r--r--app/models/project_team.rb2
-rw-r--r--app/models/repository.rb71
-rw-r--r--app/services/deploy_keys/create_service.rb7
-rw-r--r--app/services/gpg_keys/create_service.rb9
-rw-r--r--app/services/keys/base_service.rb13
-rw-r--r--app/services/keys/create_service.rb9
-rw-r--r--app/services/merge_requests/merge_service.rb10
-rw-r--r--app/services/projects/update_service.rb5
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml8
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml24
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml12
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml24
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml22
-rw-r--r--app/views/profiles/preferences/show.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_download.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml3
-rw-r--r--app/views/projects/branches/_delete_protected_modal.html.haml9
-rw-r--r--app/views/projects/buttons/_download.html.haml30
-rw-r--r--app/views/projects/compare/_form.html.haml20
-rw-r--r--app/views/projects/compare/index.html.haml18
-rw-r--r--app/views/projects/diffs/_stats.html.haml4
-rw-r--r--app/views/projects/new.html.haml4
-rw-r--r--app/views/projects/runners/_form.html.haml2
-rw-r--r--app/views/shared/issuable/_filter.html.haml4
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml5
-rw-r--r--changelogs/unreleased/21331-improve-confusing-compare-page.yml5
-rw-r--r--changelogs/unreleased/24121_extract_yet_another_users_finder.yml5
-rw-r--r--changelogs/unreleased/34510-board-issues-sql-speedup.yml5
-rw-r--r--changelogs/unreleased/35917_create_services_for_keys.yml4
-rw-r--r--changelogs/unreleased/35978-milestone-title.yml5
-rw-r--r--changelogs/unreleased/37576-renamed-files-have-escaped-html-for-the-inline-diff-in-the-header.yml5
-rw-r--r--changelogs/unreleased/37590-pipelines-mr.yml5
-rw-r--r--changelogs/unreleased/37759-also-treat-newlines-as-separator.yml5
-rw-r--r--changelogs/unreleased/change-dashed-border-button-color.yml5
-rw-r--r--changelogs/unreleased/ci-environment-status-performance.yml5
-rw-r--r--changelogs/unreleased/disallow-null-values-for-environments-project-id.yml5
-rw-r--r--changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml5
-rw-r--r--changelogs/unreleased/fix-sidebar-with-scrollbars.yml5
-rw-r--r--changelogs/unreleased/issue_37640.yml6
-rw-r--r--changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml5
-rw-r--r--changelogs/unreleased/milestone-avatar-issuable-link.yml5
-rw-r--r--changelogs/unreleased/projects-controller-show.yml5
-rw-r--r--changelogs/unreleased/refactor-animate-js.yml5
-rw-r--r--changelogs/unreleased/replace_project_archived-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_commits_revert-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_snippets-feature.yml5
-rw-r--r--changelogs/unreleased/replace_search-feature.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-discussion-json.yml5
-rw-r--r--changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml5
-rw-r--r--changelogs/unreleased/winh-protected-branch-modal-merged.yml5
-rw-r--r--config/dependency_decisions.yml36
-rw-r--r--config/initializers/doorkeeper.rb2
-rw-r--r--config/initializers/static_files.rb2
-rw-r--r--db/migrate/20170828135939_migrate_user_external_mail_data.rb57
-rw-r--r--db/migrate/20170913131410_environments_project_id_not_null.rb16
-rw-r--r--db/migrate/20170914135630_add_index_for_recent_push_events.rb40
-rw-r--r--db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb37
-rw-r--r--db/post_migrate/20170913180600_fix_projects_without_project_feature.rb33
-rw-r--r--db/schema.rb6
-rw-r--r--doc/administration/gitaly/index.md4
-rw-r--r--doc/administration/reply_by_email.md31
-rw-r--r--doc/api/tags.md2
-rw-r--r--doc/api/wikis.md2
-rw-r--r--doc/ci/environments.md100
-rw-r--r--doc/ci/img/environments_monitoring.pngbin243491 -> 76086 bytes
-rw-r--r--doc/ci/quick_start/README.md8
-rw-r--r--doc/ci/runners/README.md14
-rw-r--r--doc/ci/ssh_keys/README.md2
-rw-r--r--doc/ci/triggers/README.md6
-rw-r--r--doc/ci/variables/README.md18
-rw-r--r--doc/ci/yaml/README.md13
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx_ingress.md8
-rw-r--r--doc/user/project/pipelines/settings.md2
-rw-r--r--[-rwxr-xr-x]doc/user/project/repository/img/compare_branches.pngbin35999 -> 206831 bytes
-rw-r--r--doc/user/search/index.md4
-rw-r--r--features/project/archived.feature30
-rw-r--r--features/project/commits/revert.feature31
-rw-r--r--features/project/snippets.feature35
-rw-r--r--features/search.feature100
-rw-r--r--features/steps/project/archived.rb36
-rw-r--r--features/steps/project/commits/revert.rb42
-rw-r--r--features/steps/project/snippets.rb100
-rw-r--r--features/steps/search.rb116
-rw-r--r--lib/api/users.rb2
-rw-r--r--lib/gitlab/auth.rb24
-rw-r--r--lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb41
-rw-r--r--lib/gitlab/database/migration_helpers.rb88
-rw-r--r--lib/gitlab/database/read_only_relation.rb16
-rw-r--r--lib/gitlab/diff/inline_diff_marker.rb3
-rw-r--r--lib/gitlab/git/operation_service.rb19
-rw-r--r--lib/gitlab/git/repository.rb57
-rw-r--r--lib/gitlab/git/user.rb (renamed from lib/gitlab/git/committer.rb)10
-rw-r--r--lib/gitlab/group_hierarchy.rb15
-rw-r--r--lib/gitlab/o_auth/auth_hash.rb2
-rw-r--r--lib/gitlab/url_sanitizer.rb25
-rw-r--r--spec/controllers/boards/issues_controller_spec.rb11
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb32
-rw-r--r--spec/features/groups/merge_requests_spec.rb2
-rw-r--r--spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb22
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb8
-rw-r--r--spec/features/milestones/show_spec.rb4
-rw-r--r--spec/features/profiles/user_visits_profile_account_page_spec.rb3
-rw-r--r--spec/features/profiles/user_visits_profile_authentication_log_page_spec.rb16
-rw-r--r--spec/features/profiles/user_visits_profile_authentication_log_spec.rb15
-rw-r--r--spec/features/profiles/user_visits_profile_page_spec.rb16
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb3
-rw-r--r--spec/features/profiles/user_visits_profile_spec.rb15
-rw-r--r--spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb3
-rw-r--r--spec/features/projects/commit/user_reverts_commit_spec.rb56
-rw-r--r--spec/features/projects/diffs/diff_show_spec.rb13
-rw-r--r--spec/features/projects/snippets/user_comments_on_snippet_spec.rb25
-rw-r--r--spec/features/projects/snippets/user_deletes_snippet_spec.rb20
-rw-r--r--spec/features/projects/snippets/user_updates_snippet_spec.rb25
-rw-r--r--spec/features/projects/snippets/user_views_snippets_spec.rb20
-rw-r--r--spec/features/projects/user_archives_project_spec.rb43
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb67
-rw-r--r--spec/features/search/user_searches_for_comments_spec.rb47
-rw-r--r--spec/features/search/user_searches_for_commits_spec.rb49
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb76
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb51
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb51
-rw-r--r--spec/features/search/user_searches_for_projects_spec.rb36
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb35
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb90
-rw-r--r--spec/features/search/user_uses_search_filters_spec.rb52
-rw-r--r--spec/features/search_spec.rb310
-rw-r--r--spec/finders/autocomplete_users_finder_spec.rb97
-rw-r--r--spec/helpers/diff_helper_spec.rb4
-rw-r--r--spec/initializers/doorkeeper_spec.rb4
-rw-r--r--spec/javascripts/filtered_search/dropdown_user_spec.js2
-rw-r--r--spec/javascripts/fly_out_nav_spec.js9
-rw-r--r--spec/lib/gitlab/auth_spec.rb10
-rw-r--r--spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb40
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb122
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb10
-rw-r--r--spec/lib/gitlab/git/hooks_service_spec.rb8
-rw-r--r--spec/lib/gitlab/git/user_spec.rb (renamed from spec/lib/gitlab/git/committer_spec.rb)2
-rw-r--r--spec/lib/gitlab/group_hierarchy_spec.rb15
-rw-r--r--spec/lib/gitlab/o_auth/auth_hash_spec.rb7
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb9
-rw-r--r--spec/migrations/delete_conflicting_redirect_routes_spec.rb58
-rw-r--r--spec/models/ci/pipeline_spec.rb20
-rw-r--r--spec/models/commit_spec.rb7
-rw-r--r--spec/models/gpg_key_spec.rb12
-rw-r--r--spec/models/key_spec.rb12
-rw-r--r--spec/models/project_services/pipelines_email_service_spec.rb37
-rw-r--r--spec/models/project_spec.rb56
-rw-r--r--spec/models/repository_spec.rb46
-rw-r--r--spec/requests/api/projects_spec.rb6
-rw-r--r--spec/services/deploy_keys/create_service_spec.rb12
-rw-r--r--spec/services/gpg_keys/create_service_spec.rb21
-rw-r--r--spec/services/keys/create_service_spec.rb21
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb4
-rw-r--r--spec/services/notification_service_spec.rb1
-rw-r--r--spec/services/projects/update_service_spec.rb23
-rw-r--r--spec/spec_helper.rb2
-rw-r--r--spec/support/matchers/navigation_matcher.rb6
-rw-r--r--spec/support/query_recorder.rb36
-rw-r--r--spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb (renamed from spec/support/project_features_apply_to_issuables_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/features/search_shared_examples.rb5
-rw-r--r--spec/support/stub_gitlab_calls.rb4
-rw-r--r--spec/support/test_env.rb86
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb24
-rw-r--r--spec/views/shared/milestones/_issuable.html.haml.rb19
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb4
-rw-r--r--yarn.lock162
222 files changed, 2881 insertions, 1499 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f7ec0591086..76f1fd8a4bf 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-60.0-node-7.1-postgresql-9.6"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-61.0-node-8.x-yarn-1.0-postgresql-9.6"
.default-cache: &default-cache
key: "ruby-233-with-yarn"
@@ -191,6 +191,9 @@ review-docs-deploy:
stage: build
environment:
name: review-docs/$CI_COMMIT_REF_NAME
+ # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
+ # Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
+ url: http://$CI_COMMIT_REF_SLUG-built-from-ce-ee.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
script:
- gem install gitlab --no-doc
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1f25171e8a6..dfb2ce0099a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -286,7 +286,10 @@ might be edited to make them small and simple.
Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker.
-For changes in the interface, it can be helpful to create a mockup first.
+For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should
+be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may
+need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself.
+
If you want to create something yourself, consider opening an issue first to
discuss whether it is interesting to include this in GitLab.
diff --git a/Gemfile.lock b/Gemfile.lock
index a6dffd1d5b8..8930680c563 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -358,7 +358,7 @@ GEM
rake
grape_logging (1.7.0)
grape
- grpc (1.4.5)
+ grpc (1.6.0)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
haml (4.0.7)
diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js
index af8bcdc1794..cbc28374b80 100644
--- a/app/assets/javascripts/branches/branches_delete_modal.js
+++ b/app/assets/javascripts/branches/branches_delete_modal.js
@@ -7,6 +7,7 @@ class DeleteModal {
this.$branchName = $('.js-branch-name', this.$modal);
this.$confirmInput = $('.js-delete-branch-input', this.$modal);
this.$deleteBtn = $('.js-delete-branch', this.$modal);
+ this.$notMerged = $('.js-not-merged', this.$modal);
this.bindEvents();
}
@@ -16,8 +17,10 @@ class DeleteModal {
}
setModalData(e) {
- this.branchName = e.currentTarget.dataset.branchName || '';
- this.deletePath = e.currentTarget.dataset.deletePath || '';
+ const branchData = e.currentTarget.dataset;
+ this.branchName = branchData.branchName || '';
+ this.deletePath = branchData.deletePath || '';
+ this.isMerged = !!branchData.isMerged;
this.updateModal();
}
@@ -30,6 +33,7 @@ class DeleteModal {
this.$confirmInput.val('');
this.$deleteBtn.attr('href', this.deletePath);
this.$deleteBtn.attr('disabled', true);
+ this.$notMerged.toggleClass('hidden', this.isMerged);
}
}
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index 298f737a2bc..497c23f014f 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -4,6 +4,8 @@
import Vue from 'vue';
+import '../mixins/discussion';
+
const JumpToDiscussion = Vue.extend({
mixins: [DiscussionMixins],
props: {
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js
index 96e5a440357..fe7cf8f5fc1 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js
@@ -4,6 +4,8 @@
import Vue from 'vue';
+import '../mixins/discussion';
+
window.ResolveCount = Vue.extend({
mixins: [DiscussionMixins],
props: {
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index 7246ccbb281..720fbc87ea0 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -15,6 +15,7 @@ class DropdownUser extends gl.FilteredSearchDropdown {
params: {
per_page: 20,
active: true,
+ group_id: this.getGroupId(),
project_id: this.getProjectId(),
current_user: true,
},
@@ -47,6 +48,10 @@ class DropdownUser extends gl.FilteredSearchDropdown {
super.renderContent(forceShowList);
}
+ getGroupId() {
+ return this.input.getAttribute('data-group-id');
+ }
+
getProjectId() {
return this.input.getAttribute('data-project-id');
}
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index ad8254167a2..157280d66e3 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -77,10 +77,11 @@ export const hideMenu = (el) => {
export const moveSubItemsToPosition = (el, subItems) => {
const boundingRect = el.getBoundingClientRect();
const top = calculateTop(boundingRect, subItems.offsetHeight);
+ const left = sidebar ? sidebar.offsetWidth : 50;
const isAbove = top < boundingRect.top;
subItems.classList.add('fly-out-list');
- subItems.style.transform = `translate3d(0, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign
+ subItems.style.transform = `translate3d(${left}px, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign
const subItemsRect = subItems.getBoundingClientRect();
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 7d7f91227f9..2538d9c2093 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -127,13 +127,6 @@ import DropdownUtils from './filtered_search/dropdown_utils';
$('.has-tooltip', $value).tooltip({
container: 'body'
});
- return $value.find('a').each(function(i) {
- return setTimeout((function(_this) {
- return function() {
- return gl.animate.animate($(_this), 'pulse');
- };
- })(this), 200 * i);
- });
});
};
$dropdown.glDropdown({
diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js
deleted file mode 100644
index d93c1d0da59..00000000000
--- a/app/assets/javascripts/lib/utils/animate.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, no-void, prefer-template, no-var, new-cap, prefer-arrow-callback, consistent-return, max-len */
-(function() {
- (function(w) {
- if (w.gl == null) {
- w.gl = {};
- }
- if (gl.animate == null) {
- gl.animate = {};
- }
- gl.animate.animate = function($el, animation, options, done) {
- if ((options != null ? options.cssStart : void 0) != null) {
- $el.css(options.cssStart);
- }
- $el.removeClass(animation + ' animated').addClass(animation + ' animated').one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function() {
- $(this).removeClass(animation + ' animated');
- if (done != null) {
- done();
- }
- if ((options != null ? options.cssEnd : void 0) != null) {
- $el.css(options.cssEnd);
- }
- });
- };
- gl.animate.animateEach = function($els, animation, time, options, done) {
- var dfd;
- dfd = $.Deferred();
- if (!$els.length) {
- dfd.resolve();
- }
- $els.each(function(i) {
- setTimeout((function(_this) {
- return function() {
- var $this;
- $this = $(_this);
- return gl.animate.animate($this, animation, options, function() {
- if (i === $els.length - 1) {
- dfd.resolve();
- if (done != null) {
- return done();
- }
- }
- });
- };
- })(this), time * i);
- });
- return dfd.promise();
- };
- })(window);
-}).call(window);
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 0bc31a56684..0f84470828a 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -39,7 +39,6 @@ import './commit/file';
import './commit/image_file';
// lib/utils
-import './lib/utils/animate';
import './lib/utils/bootstrap_linked_tabs';
import './lib/utils/common_utils';
import './lib/utils/datetime_utility';
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 04579058688..4675b1fcb8f 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -45,7 +45,7 @@ import _ from 'underscore';
if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
- collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
+ collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
@@ -208,6 +208,7 @@ import _ from 'underscore';
if (data.milestone != null) {
data.milestone.full_path = _this.currentProject.full_path;
data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date);
+ data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else {
diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js
index cea4f35096a..f2eb2338a1e 100644
--- a/app/assets/javascripts/new_sidebar.js
+++ b/app/assets/javascripts/new_sidebar.js
@@ -15,7 +15,6 @@ export default class NewNavSidebar {
this.$openSidebar = $('.toggle-mobile-nav');
this.$closeSidebar = $('.close-nav-button');
this.$sidebarToggle = $('.js-toggle-sidebar');
- this.$topLevelLinks = $('.sidebar-top-level-items > li > a');
}
bindEvents() {
@@ -56,10 +55,6 @@ export default class NewNavSidebar {
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
}
NewNavSidebar.setCollapsedCookie(collapsed);
-
- this.$topLevelLinks.attr('title', function updateTopLevelTitle() {
- return collapsed ? this.getAttribute('aria-label') : '';
- });
}
render() {
diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue
index 60c172321d1..feb3e73194b 100644
--- a/app/assets/javascripts/notes/components/issue_note_actions.vue
+++ b/app/assets/javascripts/notes/components/issue_note_actions.vue
@@ -86,7 +86,7 @@
<div class="note-actions">
<span
v-if="accessLevel"
- class="note-role">{{accessLevel}}</span>
+ class="note-role note-role-access">{{accessLevel}}</span>
<div
v-if="canAddAwardEmoji"
class="note-actions-item">
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index e3daa8cf949..e754f6c4460 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
/* global Mousetrap */
import Cookies from 'js-cookie';
+import Mousetrap from 'mousetrap';
import findAndFollowLink from './shortcuts_dashboard_navigation';
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 71f764923ff..f844d6f1d5a 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -158,11 +158,23 @@
box-shadow: inset 4px 0 0 $color-700;
> a {
- color: $color-900;
+ color: $color-800;
}
svg {
- fill: $color-900;
+ fill: $color-800;
+ }
+ }
+
+ .sidebar-top-level-items > li.active .badge {
+ color: $color-800;
+ }
+
+ .nav-links li.active a {
+ border-bottom-color: $color-500;
+
+ .badge {
+ font-weight: $gl-font-weight-bold;
}
}
}
@@ -261,5 +273,9 @@ body {
fill: $theme-gray-900;
}
}
+
+ .sidebar-top-level-items > li.active .badge {
+ color: $theme-gray-900;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 5ffa67a1220..2f7717760ec 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -328,7 +328,7 @@
border-bottom: 1px solid $border-color;
transition: padding $sidebar-transition-duration;
text-align: center;
- margin-top: $header-height;
+ margin-top: $new-navbar-height;
.container-fluid {
position: relative;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index ef58382ba41..48dc25d343b 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -78,16 +78,16 @@
.right-sidebar {
border-left: 1px solid $border-color;
- height: calc(100% - #{$header-height});
+ height: calc(100% - #{$new-navbar-height});
&.affix {
position: fixed;
- top: $header-height;
+ top: $new-navbar-height;
}
}
.with-performance-bar .right-sidebar.affix {
- top: $header-height + $performance-bar-height;
+ top: $new-navbar-height + $performance-bar-height;
}
@mixin maintain-sidebar-dimensions {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 3857226cddb..a3da9fd44e8 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -13,6 +13,7 @@ $sidebar-breakpoint: 1024px;
$darken-normal-factor: 7%;
$darken-dark-factor: 10%;
$darken-border-factor: 5%;
+$darken-border-dashed-factor: 25%;
$white-light: #fff;
$white-normal: #f0f0f0;
@@ -134,6 +135,7 @@ $border-white-normal: darken($white-normal, $darken-border-factor);
$border-gray-light: darken($gray-light, $darken-border-factor);
$border-gray-normal: darken($gray-normal, $darken-border-factor);
+$border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor);
$border-gray-dark: darken($white-normal, $darken-border-factor);
/*
diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss
index e4c12e46056..8030854e527 100644
--- a/app/assets/stylesheets/new_sidebar.scss
+++ b/app/assets/stylesheets/new_sidebar.scss
@@ -3,8 +3,6 @@
@import "bootstrap/variables";
$active-background: rgba(0, 0, 0, .04);
-$active-border: $indigo-500;
-$active-color: $indigo-700;
$active-hover-background: $active-background;
$active-hover-color: $gl-text-color;
$inactive-badge-background: rgba(0, 0, 0, .08);
@@ -107,7 +105,8 @@ $new-sidebar-collapsed-width: 50px;
}
&.sidebar-icons-only {
- width: $new-sidebar-collapsed-width;
+ width: auto;
+ min-width: $new-sidebar-collapsed-width;
.nav-sidebar-inner-scroll {
overflow-x: hidden;
@@ -126,6 +125,10 @@ $new-sidebar-collapsed-width: 50px;
.fly-out-top-item {
display: block;
}
+
+ .avatar-container {
+ margin-right: 0;
+ }
}
&.nav-sidebar-expanded {
@@ -189,7 +192,7 @@ $new-sidebar-collapsed-width: 50px;
.nav-sidebar-inner-scroll {
height: 100%;
width: 100%;
- overflow: auto;
+ overflow: scroll;
}
.with-performance-bar .nav-sidebar {
@@ -217,7 +220,6 @@ $new-sidebar-collapsed-width: 50px;
&:hover,
&:focus {
background: $active-background;
- color: $active-color;
}
}
}
@@ -251,7 +253,7 @@ $new-sidebar-collapsed-width: 50px;
@media (min-width: $screen-sm-min) {
position: fixed;
top: 0;
- left: $new-sidebar-width;
+ left: 0;
min-width: 150px;
margin-top: -1px;
padding: 4px 1px;
@@ -317,7 +319,6 @@ $new-sidebar-collapsed-width: 50px;
}
.badge {
- color: $active-color;
font-weight: $gl-font-weight-bold;
}
@@ -390,10 +391,6 @@ $new-sidebar-collapsed-width: 50px;
}
.sidebar-sub-level-items {
- @media (min-width: $screen-sm-min) {
- left: $new-sidebar-collapsed-width;
- }
-
&:not(.flyout-list) {
display: none;
}
@@ -494,13 +491,3 @@ $new-sidebar-collapsed-width: 50px;
.with-performance-bar .boards-list {
height: calc(100vh - #{$new-navbar-height} - #{$performance-bar-height});
}
-
-
-// Change color of all horizontal tabs to match the new indigo color
-.nav-links li.active a {
- border-bottom-color: $active-border;
-
- .badge {
- font-weight: $gl-font-weight-bold;
- }
-}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 50ec5110bf1..359dd388d05 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -64,10 +64,10 @@
color: $gl-text-color;
position: sticky;
position: -webkit-sticky;
- top: $header-height;
+ top: $new-navbar-height;
&.affix {
- top: $header-height;
+ top: $new-navbar-height;
}
// with sidebar
@@ -174,10 +174,10 @@
.with-performance-bar .build-page {
.top-bar {
- top: $header-height + $performance-bar-height;
+ top: $new-navbar-height + $performance-bar-height;
&.affix {
- top: $header-height + $performance-bar-height;
+ top: $new-navbar-height + $performance-bar-height;
}
}
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 54c3c0173ae..951580ea1fe 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -634,8 +634,16 @@
padding-top: 8px;
padding-bottom: 8px;
}
+
+ .diff-changed-file {
+ display: flex;
+ align-items: center;
+ }
}
.diff-file-changes-path {
- @include str-truncated(78%);
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index d8a15faf7e9..d01ee4b033c 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -449,6 +449,12 @@
}
}
}
+
+ .milestone-title span {
+ @include str-truncated(100%);
+ display: block;
+ margin: 0 4px;
+ }
}
a {
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index d4dc43035eb..cf5f933a762 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -95,6 +95,8 @@
}
.omniauth-container {
+ font-size: 13px;
+
p {
margin: 0;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 296b6310552..9d03a042aa3 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -209,6 +209,11 @@
}
.stage-cell {
+ @media (min-width: $screen-md-min) {
+ min-width: 148px;
+ margin-right: -4px;
+ }
+
.mini-pipeline-graph-dropdown-toggle svg {
height: $ci-action-icon-size;
width: $ci-action-icon-size;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 94e4f4334d4..6400b72742c 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -752,7 +752,7 @@ a.deploy-project-label {
}
li.missing {
- border: 1px dashed $border-gray-normal;
+ border: 1px dashed $border-gray-normal-dashed;
border-radius: $border-radius-default;
a {
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 69abb13add4..7dfcf7b7d9c 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -71,6 +71,11 @@
height: 100%;
.monaco-editor.vs {
+ .current-line {
+ border: none;
+ background: $well-light-border;
+ }
+
.line-numbers {
cursor: pointer;
@@ -84,6 +89,13 @@
}
}
+ .blob-no-preview {
+ .vertical-center {
+ justify-content: center;
+ width: 100%;
+ }
+ }
+
&.edit-mode {
.blob-viewer-container {
overflow: hidden;
@@ -103,7 +115,7 @@
overflow: auto;
> div,
- .file-content {
+ .file-content:not(.wiki) {
display: flex;
}
diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb
index e5cba774dcb..a7ab481519d 100644
--- a/app/controllers/admin/deploy_keys_controller.rb
+++ b/app/controllers/admin/deploy_keys_controller.rb
@@ -10,9 +10,8 @@ class Admin::DeployKeysController < Admin::ApplicationController
end
def create
- @deploy_key = deploy_keys.new(create_params.merge(user: current_user))
-
- if @deploy_key.save
+ @deploy_key = DeployKeys::CreateService.new(current_user, create_params.merge(public: true)).execute
+ if @deploy_key.persisted?
redirect_to admin_deploy_keys_path
else
render 'new'
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
index cbc7a14ae83..7eb8f758807 100644
--- a/app/controllers/admin/labels_controller.rb
+++ b/app/controllers/admin/labels_controller.rb
@@ -29,7 +29,7 @@ class Admin::LabelsController < Admin::ApplicationController
@label = Labels::UpdateService.new(label_params).execute(@label)
if @label.valid?
- redirect_to admin_labels_path, notice: 'label was successfully updated.'
+ redirect_to admin_labels_path, notice: 'Label was successfully updated.'
else
render :edit
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index dfc8bd0ba81..10e8e54f402 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -3,31 +3,10 @@ class AutocompleteController < ApplicationController
skip_before_action :authenticate_user!, only: [:users, :award_emojis]
before_action :load_project, only: [:users]
- before_action :find_users, only: [:users]
+ before_action :load_group, only: [:users]
def users
- @users ||= User.none
- @users = @users.active
- @users = @users.reorder(:name)
- @users = @users.search(params[:search]) if params[:search].present?
- @users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present?
- @users = @users.page(params[:page]).per(params[:per_page])
-
- if params[:todo_filter].present? && current_user
- @users = @users.todo_authors(current_user.id, params[:todo_state_filter])
- end
-
- if params[:search].blank?
- # Include current user if available to filter by "Me"
- if params[:current_user].present? && current_user
- @users = [current_user, *@users].uniq
- end
-
- if params[:author_id].present? && current_user
- author = User.find_by_id(params[:author_id])
- @users = [author, *@users].uniq if author
- end
- end
+ @users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute
render json: @users, only: [:name, :username, :id], methods: [:avatar_url]
end
@@ -60,26 +39,14 @@ class AutocompleteController < ApplicationController
private
- def find_users
- @users =
- if @project
- user_ids = @project.team.users.pluck(:id)
-
- if params[:author_id].present?
- user_ids << params[:author_id]
- end
-
- User.where(id: user_ids)
- elsif params[:group_id].present?
+ def load_group
+ @group ||= begin
+ if @project.blank? && params[:group_id].present?
group = Group.find(params[:group_id])
return render_404 unless can?(current_user, :read_group, group)
-
- group.users
- elsif current_user
- User.all
- else
- User.none
+ group
end
+ end
end
def load_project
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 8d4ec2d6d9d..0d74078645a 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -11,9 +11,15 @@ module Boards
issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute
issues = issues.page(params[:page]).per(params[:per] || 20)
make_sure_position_is_set(issues)
+ issues = issues.preload(:project,
+ :milestone,
+ :assignees,
+ labels: [:priorities],
+ notes: [:award_emoji, :author]
+ )
render json: {
- issues: serialize_as_json(issues.preload(:project)),
+ issues: serialize_as_json(issues),
size: issues.total_count
}
end
@@ -76,14 +82,13 @@ module Boards
def serialize_as_json(resource)
resource.as_json(
- labels: true,
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
+ labels: true,
include: {
project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] }
- },
- user: current_user
+ }
)
end
end
diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb
index 6779cc6ddac..689c76059f6 100644
--- a/app/controllers/profiles/gpg_keys_controller.rb
+++ b/app/controllers/profiles/gpg_keys_controller.rb
@@ -7,9 +7,9 @@ class Profiles::GpgKeysController < Profiles::ApplicationController
end
def create
- @gpg_key = current_user.gpg_keys.new(gpg_key_params)
+ @gpg_key = GpgKeys::CreateService.new(current_user, gpg_key_params).execute
- if @gpg_key.save
+ if @gpg_key.persisted?
redirect_to profile_gpg_keys_path
else
@gpg_keys = current_user.gpg_keys.select(&:persisted?)
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index f9f0e8eef83..89d6d7f1b52 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -11,9 +11,9 @@ class Profiles::KeysController < Profiles::ApplicationController
end
def create
- @key = current_user.keys.new(key_params)
+ @key = Keys::CreateService.new(current_user, key_params).execute
- if @key.save
+ if @key.persisted?
redirect_to profile_key_path(@key)
else
@keys = current_user.keys.select(&:persisted?)
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index f748d191ef4..c1cc509a748 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -38,7 +38,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def set_index_vars
- @scopes = Gitlab::Auth::AVAILABLE_SCOPES
+ @scopes = Gitlab::Auth.available_scopes
@personal_access_token = finder.build
@inactive_personal_access_tokens = finder(state: 'inactive').execute
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 193549663ac..3c8eaa24080 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -27,7 +27,7 @@ class Projects::CompareController < Projects::ApplicationController
def create
if params[:from].blank? || params[:to].blank?
- flash[:alert] = "You must select from and to branches"
+ flash[:alert] = "You must select a Source and a Target revision"
from_to_vars = {
from: params[:from].presence,
to: params[:to].presence
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index c2e621fa190..cf8829ba95b 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -22,7 +22,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def create
- @key = DeployKey.new(create_params.merge(user: current_user))
+ @key = DeployKeys::CreateService.new(current_user, create_params).execute
unless @key.valid? && @project.deploy_keys << @key
flash[:alert] = @key.errors.full_messages.join(', ').html_safe
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 8990c919ca0..42bfa4b9d4f 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -87,9 +87,9 @@ class Projects::IssuesController < Projects::ApplicationController
.inc_relations_for_view
.includes(:noteable)
.fresh
- .reject { |n| n.cross_reference_not_visible_for?(current_user) }
- prepare_notes_for_rendering(notes)
+ notes = prepare_notes_for_rendering(notes)
+ notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
discussions = Discussion.build_collection(notes, @issue)
diff --git a/app/finders/autocomplete_users_finder.rb b/app/finders/autocomplete_users_finder.rb
new file mode 100644
index 00000000000..b8f52e31926
--- /dev/null
+++ b/app/finders/autocomplete_users_finder.rb
@@ -0,0 +1,60 @@
+class AutocompleteUsersFinder
+ attr_reader :current_user, :project, :group, :search, :skip_users,
+ :page, :per_page, :author_id, :params
+
+ def initialize(params:, current_user:, project:, group:)
+ @current_user = current_user
+ @project = project
+ @group = group
+ @search = params[:search]
+ @skip_users = params[:skip_users]
+ @page = params[:page]
+ @per_page = params[:per_page]
+ @author_id = params[:author_id]
+ @params = params
+ end
+
+ def execute
+ items = find_users
+ items = items.active
+ items = items.reorder(:name)
+ items = items.search(search) if search.present?
+ items = items.where.not(id: skip_users) if skip_users.present?
+ items = items.page(page).per(per_page)
+
+ if params[:todo_filter].present? && current_user
+ items = items.todo_authors(current_user.id, params[:todo_state_filter])
+ end
+
+ if search.blank?
+ # Include current user if available to filter by "Me"
+ if params[:current_user].present? && current_user
+ items = [current_user, *items].uniq
+ end
+
+ if author_id.present? && current_user
+ author = User.find_by_id(author_id)
+ items = [author, *items].uniq if author
+ end
+ end
+
+ items
+ end
+
+ private
+
+ def find_users
+ return users_from_project if project
+ return group.users if group
+ return User.all if current_user
+
+ User.none
+ end
+
+ def users_from_project
+ user_ids = project.team.users.pluck(:id)
+ user_ids << author_id if author_id.present?
+
+ User.where(id: user_ids)
+ end
+end
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 4bd61aa8f86..62ac208f16a 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -77,4 +77,8 @@ module BoardsHelper
'max-select': dropdown_options[:data][:'max-select']
}
end
+
+ def boards_link_text
+ _("Board")
+ end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 36b79da1bde..e8efe8fab27 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -21,7 +21,7 @@ module GroupsHelper
group.ancestors.reverse.each_with_index do |parent, index|
if index > 0
- add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true), location: :before)
+ add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true, for_dropdown: true), location: :before)
else
full_title += breadcrumb_list_item group_title_link(parent, hidable: false)
end
@@ -85,8 +85,8 @@ module GroupsHelper
private
- def group_title_link(group, hidable: false, show_avatar: false)
- link_to(group_path(group), class: "group-path breadcrumb-item-text js-breadcrumb-item-text #{'hidable' if hidable}") do
+ def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false)
+ link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do
output =
if (group.try(:avatar_url) || show_avatar) && !Rails.env.test?
image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15)
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 3308ab0c259..ee701076a14 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -119,8 +119,4 @@ module TabHelper
'active' if current_controller?('oauth/applications')
end
-
- def sidebar_link(href, title: nil, css: nil, &block)
- link_to capture(&block), href, title: (title if collapsed_sidebar?), class: css, aria: { label: title }
- end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 476db384bbd..8d017b9b3b1 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -453,6 +453,10 @@ module Ci
.fabricate!
end
+ def latest_builds_with_artifacts
+ @latest_builds_with_artifacts ||= builds.latest.with_artifacts
+ end
+
private
def ci_yaml_from_repo
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 51768dd96bc..eae5eee4fee 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -28,10 +28,4 @@ class DeployKey < Key
def can_push_to?(project)
can_push? && has_access_to?(project)
end
-
- private
-
- # we don't want to notify the user for deploy keys
- def notify_user
- end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 9b05f8b1cd5..44e39e21442 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -6,7 +6,10 @@ class Environment < ActiveRecord::Base
belongs_to :project, required: true, validate: true
- has_many :deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :deployments,
+ -> (env) { where(project_id: env.project_id) },
+ dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment'
before_validation :nullify_external_url
diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb
index 1633acd4fa9..44deae4234b 100644
--- a/app/models/gpg_key.rb
+++ b/app/models/gpg_key.rb
@@ -36,7 +36,6 @@ class GpgKey < ActiveRecord::Base
before_validation :extract_fingerprint, :extract_primary_keyid
after_commit :update_invalid_gpg_signatures, on: :create
- after_commit :notify_user, on: :create
def primary_keyid
super&.upcase
@@ -107,8 +106,4 @@ class GpgKey < ActiveRecord::Base
# only allows one key
self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first
end
-
- def notify_user
- NotificationService.new.new_gpg_key(self)
- end
end
diff --git a/app/models/key.rb b/app/models/key.rb
index a6b4dcfec0d..4fa6cac2fd0 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -28,7 +28,6 @@ class Key < ActiveRecord::Base
delegate :name, :email, to: :user, prefix: true
after_commit :add_to_shell, on: :create
- after_commit :notify_user, on: :create
after_create :post_create_hook
after_commit :remove_from_shell, on: :destroy
after_destroy :post_destroy_hook
@@ -118,8 +117,4 @@ class Key < ActiveRecord::Base
"type is forbidden. Must be #{allowed_types}"
end
-
- def notify_user
- NotificationService.new.new_key(self)
- end
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 958141a7358..899028a01a0 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -127,7 +127,12 @@ class Label < ActiveRecord::Base
end
def priority(project)
- priorities.find_by(project: project).try(:priority)
+ priority = if priorities.loaded?
+ priorities.first { |p| p.project == project }
+ else
+ priorities.find_by(project: project)
+ end
+ priority.try(:priority)
end
def template?
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 4a9a23fea1f..e279d8dd8c5 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -231,6 +231,13 @@ class Namespace < ActiveRecord::Base
end
def force_share_with_group_lock_on_descendants
- descendants.update_all(share_with_group_lock: true)
+ return unless Group.supports_nested_groups?
+
+ # We can't use `descendants.update_all` since Rails will throw away the WITH
+ # RECURSIVE statement. We also can't use WHERE EXISTS since we can't use
+ # different table aliases, hence we're just using WHERE IN. Since we have a
+ # maximum of 20 nested groups this should be fine.
+ Namespace.where(id: descendants.select(:id))
+ .update_all(share_with_group_lock: true)
end
end
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index ec0ebe4d353..1f9d712ef84 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -28,7 +28,7 @@ class PersonalAccessToken < ActiveRecord::Base
protected
def validate_scopes
- unless revoked || scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) }
+ unless revoked || scopes.all? { |scope| Gitlab::Auth.available_scopes.include?(scope.to_sym) }
errors.add :scopes, "can only contain available scopes"
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index ff5638dd155..94ae0acbe1a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -161,7 +161,7 @@ class Project < ActiveRecord::Base
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
- has_one :project_feature
+ has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics'
# Container repositories need to remove data from the container registry,
@@ -190,7 +190,7 @@ class Project < ActiveRecord::Base
has_one :auto_devops, class_name: 'ProjectAutoDevops'
accepts_nested_attributes_for :variables, allow_destroy: true
- accepts_nested_attributes_for :project_feature
+ accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data
accepts_nested_attributes_for :auto_devops
@@ -1163,6 +1163,23 @@ class Project < ActiveRecord::Base
pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
end
+ def latest_successful_pipeline_for_default_branch
+ if defined?(@latest_successful_pipeline_for_default_branch)
+ return @latest_successful_pipeline_for_default_branch
+ end
+
+ @latest_successful_pipeline_for_default_branch =
+ pipelines.latest_successful_for(default_branch)
+ end
+
+ def latest_successful_pipeline_for(ref = nil)
+ if ref && ref != default_branch
+ pipelines.latest_successful_for(ref)
+ else
+ latest_successful_pipeline_for_default_branch
+ end
+ end
+
def enable_ci
project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index fb1db0255aa..bfb8d703ec9 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -41,6 +41,8 @@ class ProjectFeature < ActiveRecord::Base
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
belongs_to :project, -> { unscope(where: :pending_delete) }
+ validates :project, presence: true
+
validate :repository_children_level
default_value_for :builds_access_level, value: ENABLED, allows_nil: false
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index 9d37184be2c..6a3118a11b8 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -80,6 +80,6 @@ class PipelinesEmailService < Service
end
def retrieve_recipients(data)
- recipients.to_s.split(',').reject(&:blank?)
+ recipients.to_s.split(/[,(?:\r?\n) ]+/).reject(&:empty?)
end
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 09049824ff7..1d35426050e 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -146,7 +146,7 @@ class ProjectTeam
def member?(user, min_access_level = Gitlab::Access::GUEST)
return false unless user
- user.authorized_project?(project, min_access_level)
+ max_member_access(user.id) >= min_access_level
end
def human_max_access(user_id)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 035f85a0b46..af9911ea045 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -90,6 +90,12 @@ class Repository
)
end
+ # we need to have this method here because it is not cached in ::Git and
+ # the method is called multiple times for every request
+ def has_visible_content?
+ branch_count > 0
+ end
+
def inspect
"#<#{self.class.name}:#{@disk_path}>"
end
@@ -166,7 +172,7 @@ class Repository
end
def add_branch(user, branch_name, ref)
- branch = raw_repository.add_branch(branch_name, committer: user, target: ref)
+ branch = raw_repository.add_branch(branch_name, user: user, target: ref)
after_create_branch
@@ -176,7 +182,7 @@ class Repository
end
def add_tag(user, tag_name, target, message = nil)
- raw_repository.add_tag(tag_name, committer: user, target: target, message: message)
+ raw_repository.add_tag(tag_name, user: user, target: target, message: message)
rescue Gitlab::Git::Repository::InvalidRef
false
end
@@ -184,7 +190,7 @@ class Repository
def rm_branch(user, branch_name)
before_remove_branch
- raw_repository.rm_branch(branch_name, committer: user)
+ raw_repository.rm_branch(branch_name, user: user)
after_remove_branch
true
@@ -193,7 +199,7 @@ class Repository
def rm_tag(user, tag_name)
before_remove_tag
- raw_repository.rm_tag(tag_name, committer: user)
+ raw_repository.rm_tag(tag_name, user: user)
after_remove_tag
true
@@ -762,17 +768,23 @@ class Repository
multi_action(**options)
end
- def with_branch(user, *args)
- result = Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit|
- yield start_commit
- end
+ def with_cache_hooks
+ result = yield
+
+ return unless result
- newrev, should_run_after_create, should_run_after_create_branch = result
+ after_create if result.repo_created?
+ after_create_branch if result.branch_created?
- after_create if should_run_after_create
- after_create_branch if should_run_after_create_branch
+ result.newrev
+ end
- newrev
+ def with_branch(user, *args)
+ with_cache_hooks do
+ Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit|
+ yield start_commit
+ end
+ end
end
# rubocop:disable Metrics/ParameterLists
@@ -837,30 +849,13 @@ class Repository
end
end
- def merge(user, source, merge_request, options = {})
- with_branch(
- user,
- merge_request.target_branch) do |start_commit|
- our_commit = start_commit.sha
- their_commit = source
-
- raise 'Invalid merge target' unless our_commit
- raise 'Invalid merge source' unless their_commit
-
- merge_index = rugged.merge_commits(our_commit, their_commit)
- break if merge_index.conflicts?
-
- actual_options = options.merge(
- parents: [our_commit, their_commit],
- tree: merge_index.write_tree(rugged)
- )
-
- commit_id = create_commit(actual_options)
- merge_request.update(in_progress_merge_commit_sha: commit_id)
- commit_id
+ def merge(user, source_sha, merge_request, message)
+ with_cache_hooks do
+ raw_repository.merge(user, source_sha, merge_request.target_branch, message) do |commit_id|
+ merge_request.update(in_progress_merge_commit_sha: commit_id)
+ nil # Return value does not matter.
+ end
end
- rescue Gitlab::Git::CommitError # when merge_index.conflicts?
- false
end
def revert(
@@ -1151,12 +1146,6 @@ class Repository
Gitlab::Metrics.add_event(event, { path: full_path }.merge(tags))
end
- def create_commit(params = {})
- params[:message].delete!("\r")
-
- Rugged::Commit.create(rugged, params)
- end
-
def last_commit_for_path_by_gitaly(sha, path)
c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path)
commit(c)
diff --git a/app/services/deploy_keys/create_service.rb b/app/services/deploy_keys/create_service.rb
new file mode 100644
index 00000000000..16de3d08df2
--- /dev/null
+++ b/app/services/deploy_keys/create_service.rb
@@ -0,0 +1,7 @@
+module DeployKeys
+ class CreateService < Keys::BaseService
+ def execute
+ DeployKey.create(params.merge(user: user))
+ end
+ end
+end
diff --git a/app/services/gpg_keys/create_service.rb b/app/services/gpg_keys/create_service.rb
new file mode 100644
index 00000000000..e822a89c4d3
--- /dev/null
+++ b/app/services/gpg_keys/create_service.rb
@@ -0,0 +1,9 @@
+module GpgKeys
+ class CreateService < Keys::BaseService
+ def execute
+ key = user.gpg_keys.create(params)
+ notification_service.new_gpg_key(key) if key.persisted?
+ key
+ end
+ end
+end
diff --git a/app/services/keys/base_service.rb b/app/services/keys/base_service.rb
new file mode 100644
index 00000000000..545832d0bd4
--- /dev/null
+++ b/app/services/keys/base_service.rb
@@ -0,0 +1,13 @@
+module Keys
+ class BaseService
+ attr_accessor :user, :params
+
+ def initialize(user, params)
+ @user, @params = user, params
+ end
+
+ def notification_service
+ NotificationService.new
+ end
+ end
+end
diff --git a/app/services/keys/create_service.rb b/app/services/keys/create_service.rb
new file mode 100644
index 00000000000..e2e5a6c46c5
--- /dev/null
+++ b/app/services/keys/create_service.rb
@@ -0,0 +1,9 @@
+module Keys
+ class CreateService < ::Keys::BaseService
+ def execute
+ key = user.keys.create(params)
+ notification_service.new_key(key) if key.persisted?
+ key
+ end
+ end
+end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index b2b6c5627fb..07cbd8f92a9 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -38,15 +38,9 @@ module MergeRequests
private
def commit
- committer = repository.user_to_committer(current_user)
+ message = params[:commit_message] || merge_request.merge_commit_message
- options = {
- message: params[:commit_message] || merge_request.merge_commit_message,
- author: committer,
- committer: committer
- }
-
- commit_id = repository.merge(current_user, source, merge_request, options)
+ commit_id = repository.merge(current_user, source, merge_request, message)
raise MergeError, 'Conflicts detected during merge' unless commit_id
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index cb4ffcab778..13e292a18bf 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -24,7 +24,10 @@ module Projects
success
else
- error('Project could not be updated!')
+ model_errors = project.errors.full_messages.to_sentence
+ error_message = model_errors.presence || 'Project could not be updated!'
+
+ error(error_message)
end
end
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index bfd7dd25a7d..546cec4d565 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -7,6 +7,8 @@
%span.light
- has_icon = provider_has_icon?(provider)
= link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}"
- %fieldset.prepend-top-10
- = check_box_tag :remember_me
- = label_tag :remember_me, 'Remember me'
+ %fieldset.prepend-top-10.checkbox.remember-me
+ %label
+ = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
+ %span
+ Remember me
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index fcebb385a65..615238b94ad 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -7,7 +7,7 @@
.sidebar-context-title Admin Area
%ul.sidebar-top-level-items
= nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: {class: 'home'}) do
- = sidebar_link admin_root_path, title: _('Overview'), css: 'shortcuts-tree' do
+ = link_to admin_root_path, class: 'shortcuts-tree' do
.nav-icon-container
= custom_icon('overview')
%span.nav-item-name
@@ -53,7 +53,7 @@
ConvDev Index
= nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do
- = sidebar_link admin_system_info_path, title: _('Monitoring') do
+ = link_to admin_system_info_path do
.nav-icon-container
= custom_icon('monitoring')
%span.nav-item-name
@@ -87,7 +87,7 @@
Requests Profiles
= nav_link(controller: :broadcast_messages) do
- = sidebar_link admin_broadcast_messages_path, title: _('Messages') do
+ = link_to admin_broadcast_messages_path do
.nav-icon-container
= custom_icon('messages')
%span.nav-item-name
@@ -99,7 +99,7 @@
#{ _('Messages') }
= nav_link(controller: [:hooks, :hook_logs]) do
- = sidebar_link admin_hooks_path, title: _('Hooks') do
+ = link_to admin_hooks_path do
.nav-icon-container
= custom_icon('system_hooks')
%span.nav-item-name
@@ -111,7 +111,7 @@
#{ _('System Hooks') }
= nav_link(controller: :applications) do
- = sidebar_link admin_applications_path, title: _('Applications') do
+ = link_to admin_applications_path do
.nav-icon-container
= custom_icon('applications')
%span.nav-item-name
@@ -123,7 +123,7 @@
#{ _('Applications') }
= nav_link(controller: :abuse_reports) do
- = sidebar_link admin_abuse_reports_path, title: _("Abuse Reports") do
+ = link_to admin_abuse_reports_path do
.nav-icon-container
= custom_icon('abuse_reports')
%span.nav-item-name
@@ -138,7 +138,7 @@
- if akismet_enabled?
= nav_link(controller: :spam_logs) do
- = sidebar_link admin_spam_logs_path, title: _("Spam Logs") do
+ = link_to admin_spam_logs_path do
.nav-icon-container
= custom_icon('spam_logs')
%span.nav-item-name
@@ -150,7 +150,7 @@
#{ _('Spam Logs') }
= nav_link(controller: :deploy_keys) do
- = sidebar_link admin_deploy_keys_path, title: _('Deploy Keys') do
+ = link_to admin_deploy_keys_path do
.nav-icon-container
= custom_icon('key')
%span.nav-item-name
@@ -162,7 +162,7 @@
#{ _('Deploy Keys') }
= nav_link(controller: :services) do
- = sidebar_link admin_application_settings_services_path, title: _('Service Templates') do
+ = link_to admin_application_settings_services_path do
.nav-icon-container
= custom_icon('service_templates')
%span.nav-item-name
@@ -174,7 +174,7 @@
#{ _('Service Templates') }
= nav_link(controller: :labels) do
- = sidebar_link admin_labels_path, title: _('Labels') do
+ = link_to admin_labels_path do
.nav-icon-container
= custom_icon('labels')
%span.nav-item-name
@@ -186,7 +186,7 @@
#{ _('Labels') }
= nav_link(controller: :appearances) do
- = sidebar_link admin_appearances_path, title: _('Appearances') do
+ = link_to admin_appearances_path do
.nav-icon-container
= custom_icon('appearance')
%span.nav-item-name
@@ -198,7 +198,7 @@
#{ _('Appearance') }
= nav_link(controller: :application_settings) do
- = sidebar_link admin_application_settings_path, title: _('Settings') do
+ = link_to admin_application_settings_path do
.nav-icon-container
= custom_icon('settings')
%span.nav-item-name
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index e01dfa7c854..cb44c012f56 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -11,7 +11,7 @@
= @group.name
%ul.sidebar-top-level-items
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do
- = sidebar_link group_path(@group), title: _('Group overview') do
+ = link_to group_path(@group) do
.nav-icon-container
= custom_icon('project')
%span.nav-item-name
@@ -34,7 +34,7 @@
Activity
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
- = sidebar_link issues_group_path(@group), title: _('Issues') do
+ = link_to issues_group_path(@group) do
.nav-icon-container
= custom_icon('issues')
%span.nav-item-name
@@ -64,7 +64,7 @@
Milestones
= nav_link(path: 'groups#merge_requests') do
- = sidebar_link merge_requests_group_path(@group), title: _('Merge Requests') do
+ = link_to merge_requests_group_path(@group) do
.nav-icon-container
= custom_icon('mr_bold')
%span.nav-item-name
@@ -77,19 +77,19 @@
#{ _('Merge Requests') }
%span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests.count)
= nav_link(path: 'group_members#index') do
- = sidebar_link group_group_members_path(@group), title: _('Members') do
+ = link_to group_group_members_path(@group) do
.nav-icon-container
= custom_icon('members')
%span.nav-item-name
Members
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do
- = link_to merge_requests_group_path(@group) do
+ = link_to group_group_members_path(@group) do
%strong.fly-out-top-item-name
#{ _('Members') }
- if current_user && can?(current_user, :admin_group, @group)
= nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do
- = sidebar_link edit_group_path(@group), title: _('Settings') do
+ = link_to edit_group_path(@group) do
.nav-icon-container
= custom_icon('settings')
%span.nav-item-name
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 4c26d107ea7..2c402591f62 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -7,7 +7,7 @@
.sidebar-context-title User Settings
%ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
- = sidebar_link profile_path, title: _('Profile Settings') do
+ = link_to profile_path do
.nav-icon-container
= custom_icon('profile')
%span.nav-item-name
@@ -18,7 +18,7 @@
%strong.fly-out-top-item-name
#{ _('Profile') }
= nav_link(controller: [:accounts, :two_factor_auths]) do
- = sidebar_link profile_account_path, title: _('Account') do
+ = link_to profile_account_path do
.nav-icon-container
= custom_icon('account')
%span.nav-item-name
@@ -30,7 +30,7 @@
#{ _('Account') }
- if current_application_settings.user_oauth_applications?
= nav_link(controller: 'oauth/applications') do
- = sidebar_link applications_profile_path, title: _('Applications') do
+ = link_to applications_profile_path do
.nav-icon-container
= custom_icon('applications')
%span.nav-item-name
@@ -41,7 +41,7 @@
%strong.fly-out-top-item-name
#{ _('Applications') }
= nav_link(controller: :chat_names) do
- = sidebar_link profile_chat_names_path, title: _('Chat') do
+ = link_to profile_chat_names_path do
.nav-icon-container
= custom_icon('chat')
%span.nav-item-name
@@ -52,7 +52,7 @@
%strong.fly-out-top-item-name
#{ _('Chat') }
= nav_link(controller: :personal_access_tokens) do
- = sidebar_link profile_personal_access_tokens_path, title: _('Access Tokens') do
+ = link_to profile_personal_access_tokens_path do
.nav-icon-container
= custom_icon('access_tokens')
%span.nav-item-name
@@ -63,7 +63,7 @@
%strong.fly-out-top-item-name
#{ _('Access Tokens') }
= nav_link(controller: :emails) do
- = sidebar_link profile_emails_path, title: _('Emails') do
+ = link_to profile_emails_path do
.nav-icon-container
= custom_icon('emails')
%span.nav-item-name
@@ -75,7 +75,7 @@
#{ _('Emails') }
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
- = sidebar_link edit_profile_password_path, title: _('Password') do
+ = link_to edit_profile_password_path do
.nav-icon-container
= custom_icon('lock')
%span.nav-item-name
@@ -86,7 +86,7 @@
%strong.fly-out-top-item-name
#{ _('Password') }
= nav_link(controller: :notifications) do
- = sidebar_link profile_notifications_path, title: _('Notifications') do
+ = link_to profile_notifications_path do
.nav-icon-container
= custom_icon('notifications')
%span.nav-item-name
@@ -97,7 +97,7 @@
%strong.fly-out-top-item-name
#{ _('Notifications') }
= nav_link(controller: :keys) do
- = sidebar_link profile_keys_path, title: _('SSH Keys') do
+ = link_to profile_keys_path do
.nav-icon-container
= custom_icon('key')
%span.nav-item-name
@@ -108,7 +108,7 @@
%strong.fly-out-top-item-name
#{ _('SSH Keys') }
= nav_link(controller: :gpg_keys) do
- = sidebar_link profile_gpg_keys_path, title: _('GPG Keys') do
+ = link_to profile_gpg_keys_path do
.nav-icon-container
= custom_icon('key_2')
%span.nav-item-name
@@ -119,7 +119,7 @@
%strong.fly-out-top-item-name
#{ _('GPG Keys') }
= nav_link(controller: :preferences) do
- = sidebar_link profile_preferences_path, title: _('Preferences') do
+ = link_to profile_preferences_path do
.nav-icon-container
= custom_icon('preferences')
%span.nav-item-name
@@ -130,7 +130,7 @@
%strong.fly-out-top-item-name
#{ _('Preferences') }
= nav_link(path: 'profiles#audit_log') do
- = sidebar_link audit_log_profile_path, title: _('Authentication log') do
+ = link_to audit_log_profile_path do
.nav-icon-container
= custom_icon('authentication_log')
%span.nav-item-name
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 9589e81c750..29f1fc6b354 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -9,7 +9,7 @@
= @project.name
%ul.sidebar-top-level-items
= nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
- = sidebar_link project_path(@project), title: _('Project overview'), css: 'shortcuts-project' do
+ = link_to project_path(@project), class: 'shortcuts-project' do
.nav-icon-container
= custom_icon('project')
%span.nav-item-name
@@ -36,7 +36,7 @@
- if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do
- = sidebar_link project_tree_path(@project), title: _('Repository'), css: 'shortcuts-tree' do
+ = link_to project_tree_path(@project), class: 'shortcuts-tree' do
.nav-icon-container
= custom_icon('doc_text')
%span.nav-item-name
@@ -82,7 +82,7 @@
- if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do
- = sidebar_link project_container_registry_index_path(@project), title: _('Container Registry'), css: 'shortcuts-container-registry' do
+ = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
.nav-icon-container
= custom_icon('container_registry')
%span.nav-item-name
@@ -90,7 +90,7 @@
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
- = sidebar_link project_issues_path(@project), title: _('Issues'), css: 'shortcuts-issues' do
+ = link_to project_issues_path(@project), class: 'shortcuts-issues' do
.nav-icon-container
= custom_icon('issues')
%span.nav-item-name
@@ -114,9 +114,9 @@
List
= nav_link(controller: :boards) do
- = link_to project_boards_path(@project), title: 'Board' do
+ = link_to project_boards_path(@project), title: boards_link_text do
%span
- Board
+ = boards_link_text
.feature-highlight.js-feature-highlight{ disabled: true, data: { trigger: 'manual', container: 'body', toggle: 'popover', placement: 'right', highlight: 'issue-boards' } }
.feature-highlight-popover-content
= render 'feature_highlight/issue_boards.svg'
@@ -144,7 +144,7 @@
- if project_nav_tab? :merge_requests
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
- = sidebar_link project_merge_requests_path(@project), title: _('Merge Requests'), css: 'shortcuts-merge_requests' do
+ = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests' do
.nav-icon-container
= custom_icon('mr_bold')
%span.nav-item-name
@@ -161,7 +161,7 @@
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do
- = sidebar_link project_pipelines_path(@project), title: _('CI / CD'), css: 'shortcuts-pipelines' do
+ = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do
.nav-icon-container
= custom_icon('pipeline')
%span.nav-item-name
@@ -205,7 +205,7 @@
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
- = sidebar_link get_project_wiki_path(@project), title: _('Wiki'), css: 'shortcuts-wiki' do
+ = link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do
.nav-icon-container
= custom_icon('wiki')
%span.nav-item-name
@@ -218,7 +218,7 @@
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
- = sidebar_link project_snippets_path(@project), title: _('Snippets'), css: 'shortcuts-snippets' do
+ = link_to project_snippets_path(@project), class: 'shortcuts-snippets' do
.nav-icon-container
= custom_icon('snippets')
%span.nav-item-name
@@ -231,7 +231,7 @@
- if project_nav_tab? :settings
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do
- = sidebar_link edit_project_path(@project), title: _('Settings'), css: 'shortcuts-tree' do
+ = link_to edit_project_path(@project), class: 'shortcuts-tree' do
.nav-icon-container
= custom_icon('settings')
%span.nav-item-name
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 69885008ecd..66d1d1e8d44 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -17,7 +17,7 @@
.preview-row
.quadrant.three
.quadrant.four
- = f.radio_button :theme_id, theme.id
+ = f.radio_button :theme_id, theme.id, checked: Gitlab::Themes.for_user(@user).id == theme.id
= theme.name
.col-sm-12
diff --git a/app/views/projects/blob/viewers/_download.html.haml b/app/views/projects/blob/viewers/_download.html.haml
index 6d1138f7959..253566c43be 100644
--- a/app/views/projects/blob/viewers/_download.html.haml
+++ b/app/views/projects/blob/viewers/_download.html.haml
@@ -1,5 +1,5 @@
.file-content.blob_file.blob-no-preview
- .center
+ .center.render-error.vertical-center
= link_to blob_raw_path do
%h1.light
= icon('download')
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 19712a8f1be..05c1d2b383c 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -43,7 +43,8 @@
data: { toggle: "modal",
target: "#modal-delete-branch",
delete_path: project_branch_path(@project, branch.name),
- branch_name: branch.name } }
+ branch_name: branch.name,
+ is_merged: ("true" if @repository.merged_to_root_ref?(branch.name)) } }
= icon("trash-o")
- else
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
diff --git a/app/views/projects/branches/_delete_protected_modal.html.haml b/app/views/projects/branches/_delete_protected_modal.html.haml
index c5888afa54d..f00a0ee6925 100644
--- a/app/views/projects/branches/_delete_protected_modal.html.haml
+++ b/app/views/projects/branches/_delete_protected_modal.html.haml
@@ -6,13 +6,18 @@
%h3.page-title
Delete protected branch
= surround "'", "'?" do
- %span.js-branch-name>[branch name]
+ %span.js-branch-name.ref-name>[branch name]
.modal-body
%p
You’re about to permanently delete the protected branch
= succeed '.' do
- %strong.js-branch-name [branch name]
+ %strong.js-branch-name.ref-name [branch name]
+ %p.js-not-merged
+ - default_branch = capture do
+ %span.ref-name= @repository.root_ref
+ = s_("Branches|This branch hasn’t been merged into %{default_branch}.").html_safe % { default_branch: default_branch }
+ = s_("Branches|To avoid data loss, consider merging this branch before deleting it.")
%p
Once you confirm and press
= succeed ',' do
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 883922dbf04..9d85e027ac9 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,4 +1,4 @@
-- pipeline = local_assigns.fetch(:pipeline) { project.pipelines.latest_successful_for(ref) }
+- pipeline = local_assigns.fetch(:pipeline) { project.latest_successful_pipeline_for(ref) }
- if !project.empty_repo? && can?(current_user, :download_code, project)
.project-action-button.dropdown.inline>
@@ -26,18 +26,16 @@
%i.fa.fa-download
%span= _('Download tar')
- - if pipeline
- - artifacts = pipeline.builds.latest.with_artifacts
- - if artifacts.any?
- %li.dropdown-header Artifacts
- - unless pipeline.latest?
- - latest_pipeline = project.pipeline_for(ref)
- %li
- .unclickable= ci_status_for_statuseable(latest_pipeline)
- %li.dropdown-header Previous Artifacts
- - artifacts.each do |job|
- %li
- = link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do
- %i.fa.fa-download
- %span
- #{ s_('DownloadArtifacts|Download') } '#{job.name}'
+ - if pipeline && pipeline.latest_builds_with_artifacts.any?
+ %li.dropdown-header Artifacts
+ - unless pipeline.latest?
+ - latest_pipeline = project.pipeline_for(ref)
+ %li
+ .unclickable= ci_status_for_statuseable(latest_pipeline)
+ %li.dropdown-header Previous Artifacts
+ - pipeline.latest_builds_with_artifacts.each do |job|
+ %li
+ = link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do
+ %i.fa.fa-download
+ %span
+ #{s_('DownloadArtifacts|Download')} '#{job.name}'
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 94b7db5eb25..a518fced2b4 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -2,22 +2,22 @@
.clearfix
- if params[:to] && params[:from]
.compare-switch-container
- = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Switch base of comparison'}
- .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
- .input-group.inline-input-group
- %span.input-group-addon from
- = hidden_field_tag :from, params[:from]
- = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
- .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag'
- = render 'shared/ref_dropdown'
- .compare-ellipsis.inline ...
+ = link_to icon('exchange'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Swap revisions'
.form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
.input-group.inline-input-group
- %span.input-group-addon to
+ %span.input-group-addon Source
= hidden_field_tag :to, params[:to]
= button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
.dropdown-toggle-text.str-truncated= params[:to] || 'Select branch/tag'
= render 'shared/ref_dropdown'
+ .compare-ellipsis.inline ...
+ .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
+ .input-group.inline-input-group
+ %span.input-group-addon Target
+ = hidden_field_tag :from, params[:from]
+ = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
+ .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag'
+ = render 'shared/ref_dropdown'
&nbsp;
= button_tag "Compare", class: "btn btn-create commits-compare-btn"
- if @merge_request.present?
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index 2632fea6eba..1ce3ad0c0fd 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -7,13 +7,19 @@
.sub-header-block
Compare Git revisions.
%br
- Fill input field with commit SHA like
- %code.ref-name 4eedf23
- or branch/tag name like
- %code.ref-name master
- and press compare button for the commits list and a code diff.
+ Choose a branch/tag (e.g.
+ = succeed ')' do
+ %code.ref-name master
+ or enter a commit SHA (e.g.
+ = succeed ')' do
+ %code.ref-name 4eedf23
+ to see what's changed or to create a merge request.
%br
- Changes are shown <b>from</b> the version in the first field <b>to</b> the version in the second field.
+ Changes are shown as if the
+ %b source
+ revision was being merged into the
+ %b target
+ revision.
.prepend-top-20
= render "form"
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index ad2d355ab4a..2de2cf9e38c 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -21,9 +21,9 @@
%ul
- diff_files.each do |diff_file|
%li
- %a{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path }
+ %a.diff-changed-file{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path }
= icon("#{diff_file_changed_icon(diff_file)} fw", class: "#{diff_file_changed_icon_color(diff_file)} append-right-5")
- %span.diff-file-changes-path= diff_file.new_path
+ %span.diff-file-changes-path.append-right-5= diff_file.new_path
.pull-right
%span.cgreen<
+#{diff_file.added_lines}
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 819392b8f0c..cc41b908946 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -16,7 +16,9 @@
New project
- if import_sources_enabled?
%p
- Create or Import your project from popular Git services
+ A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}.
+ %p
+ All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings.
.col-lg-9.js-toggle-container
= form_for @project, html: { class: 'new_project' } do |f|
.create-project-options
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index de85615c672..e660fce652f 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -11,7 +11,7 @@
.col-sm-10
.checkbox
= f.check_box :access_level, {}, 'ref_protected', 'not_protected'
- %span.light This runner will only run on pipelines trigged on protected branches
+ %span.light This runner will only run on pipelines triggered on protected branches
.form-group
= label :run_untagged, 'Run untagged jobs', class: 'control-label'
.col-sm-10
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index c4ed7f6e750..d3f0aa2d339 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -11,13 +11,13 @@
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
= dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
- placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user.try(:username), current_user: true, project_id: @project.try(:id), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
+ placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user&.username, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline
- if params[:assignee_id].present?
= hidden_field_tag(:assignee_id, params[:assignee_id])
= dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
- placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), group_id: @group&.id, selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
+ placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user&.username, null_user: true, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
.filter-item.inline.milestone-filter
= render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 0afa48b392c..9cae3f51825 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -24,9 +24,9 @@
.block.milestone
.sidebar-collapsed-icon
= icon('clock-o', 'aria-hidden': 'true')
- %span
+ %span.milestone-title
- if issuable.milestone
- %span.has-tooltip{ title: milestone_remaining_days(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } }
+ %span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_remaining_days(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } }
= issuable.milestone.title
- else
None
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 3739f4c221d..14395bcc661 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -26,6 +26,6 @@
%span.assignee-icon
- assignees.each do |assignee|
- = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: assignee.id, state: 'all' }),
+ = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, assignee_id: assignee.id, state: 'all' }),
class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
- image_tag(avatar_icon(assignee, 16), class: "avatar s16", alt: '')
diff --git a/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml b/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml
new file mode 100644
index 00000000000..245b8129de8
--- /dev/null
+++ b/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml
@@ -0,0 +1,5 @@
+---
+title: Return only group's members in user dropdowns on issuables list pages
+merge_request: 14249
+author:
+type: changed
diff --git a/changelogs/unreleased/21331-improve-confusing-compare-page.yml b/changelogs/unreleased/21331-improve-confusing-compare-page.yml
new file mode 100644
index 00000000000..469cc04930b
--- /dev/null
+++ b/changelogs/unreleased/21331-improve-confusing-compare-page.yml
@@ -0,0 +1,5 @@
+---
+title: Make the labels in the Compare form less confusing
+merge_request: 14225
+author:
+type: changed
diff --git a/changelogs/unreleased/24121_extract_yet_another_users_finder.yml b/changelogs/unreleased/24121_extract_yet_another_users_finder.yml
new file mode 100644
index 00000000000..e43e97303e2
--- /dev/null
+++ b/changelogs/unreleased/24121_extract_yet_another_users_finder.yml
@@ -0,0 +1,5 @@
+---
+title: Extract AutocompleteController#users into finder
+merge_request: 13778
+author: Maxim Rydkin, Mayra Cabrera
+type: other
diff --git a/changelogs/unreleased/34510-board-issues-sql-speedup.yml b/changelogs/unreleased/34510-board-issues-sql-speedup.yml
new file mode 100644
index 00000000000..244ff7e9dfa
--- /dev/null
+++ b/changelogs/unreleased/34510-board-issues-sql-speedup.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize the boards' issues fetching.
+merge_request: 14198
+author:
+type: other
diff --git a/changelogs/unreleased/35917_create_services_for_keys.yml b/changelogs/unreleased/35917_create_services_for_keys.yml
new file mode 100644
index 00000000000..e7cad5a11d5
--- /dev/null
+++ b/changelogs/unreleased/35917_create_services_for_keys.yml
@@ -0,0 +1,4 @@
+---
+title: creation of keys moved to services
+merge_request: 13331
+author: haseebeqx
diff --git a/changelogs/unreleased/35978-milestone-title.yml b/changelogs/unreleased/35978-milestone-title.yml
new file mode 100644
index 00000000000..1a4b71328c1
--- /dev/null
+++ b/changelogs/unreleased/35978-milestone-title.yml
@@ -0,0 +1,5 @@
+---
+title: Truncate milestone title if sidebar is collapsed
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/37576-renamed-files-have-escaped-html-for-the-inline-diff-in-the-header.yml b/changelogs/unreleased/37576-renamed-files-have-escaped-html-for-the-inline-diff-in-the-header.yml
new file mode 100644
index 00000000000..8c328eb0950
--- /dev/null
+++ b/changelogs/unreleased/37576-renamed-files-have-escaped-html-for-the-inline-diff-in-the-header.yml
@@ -0,0 +1,5 @@
+---
+title: Fix the diff file header from being html escaped for renamed files.
+merge_request: 14121
+author:
+type: fixed
diff --git a/changelogs/unreleased/37590-pipelines-mr.yml b/changelogs/unreleased/37590-pipelines-mr.yml
new file mode 100644
index 00000000000..ee609888155
--- /dev/null
+++ b/changelogs/unreleased/37590-pipelines-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Fix mini graph pipeline breakin in merge request view
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/37759-also-treat-newlines-as-separator.yml b/changelogs/unreleased/37759-also-treat-newlines-as-separator.yml
new file mode 100644
index 00000000000..6894e650c11
--- /dev/null
+++ b/changelogs/unreleased/37759-also-treat-newlines-as-separator.yml
@@ -0,0 +1,5 @@
+---
+title: Allow using newlines in pipeline email service recipients
+merge_request: 14250
+author:
+type: fixed
diff --git a/changelogs/unreleased/change-dashed-border-button-color.yml b/changelogs/unreleased/change-dashed-border-button-color.yml
new file mode 100644
index 00000000000..038bea79273
--- /dev/null
+++ b/changelogs/unreleased/change-dashed-border-button-color.yml
@@ -0,0 +1,5 @@
+---
+title: changed dashed border button color to be darker
+merge_request: !14041
+author:
+type: other
diff --git a/changelogs/unreleased/ci-environment-status-performance.yml b/changelogs/unreleased/ci-environment-status-performance.yml
new file mode 100644
index 00000000000..8812733b5a7
--- /dev/null
+++ b/changelogs/unreleased/ci-environment-status-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Constrain environment deployments to project IDs
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/disallow-null-values-for-environments-project-id.yml b/changelogs/unreleased/disallow-null-values-for-environments-project-id.yml
new file mode 100644
index 00000000000..f4a956e6724
--- /dev/null
+++ b/changelogs/unreleased/disallow-null-values-for-environments-project-id.yml
@@ -0,0 +1,5 @@
+---
+title: "Disallow NULL values for environments.project_id"
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml b/changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml
new file mode 100644
index 00000000000..3dfe4114cc9
--- /dev/null
+++ b/changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml
@@ -0,0 +1,5 @@
+---
+title: Fix docs for lightweight tag creation via API
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/fix-sidebar-with-scrollbars.yml b/changelogs/unreleased/fix-sidebar-with-scrollbars.yml
new file mode 100644
index 00000000000..e0b3851b97f
--- /dev/null
+++ b/changelogs/unreleased/fix-sidebar-with-scrollbars.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed the sidebar scrollbar overlapping links
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/issue_37640.yml b/changelogs/unreleased/issue_37640.yml
new file mode 100644
index 00000000000..d806ed64bed
--- /dev/null
+++ b/changelogs/unreleased/issue_37640.yml
@@ -0,0 +1,6 @@
+---
+title: Fix project feature being deleted when updating project with invalid visibility
+ level
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml b/changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml
new file mode 100644
index 00000000000..5a7cd42b888
--- /dev/null
+++ b/changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: "Memoize the latest builds of a pipeline on a project's homepage"
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/milestone-avatar-issuable-link.yml b/changelogs/unreleased/milestone-avatar-issuable-link.yml
new file mode 100644
index 00000000000..7915ad60fa8
--- /dev/null
+++ b/changelogs/unreleased/milestone-avatar-issuable-link.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed milestone issuable assignee link URL
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/projects-controller-show.yml b/changelogs/unreleased/projects-controller-show.yml
new file mode 100644
index 00000000000..25f4a72710b
--- /dev/null
+++ b/changelogs/unreleased/projects-controller-show.yml
@@ -0,0 +1,5 @@
+---
+title: Memoize pipelines for project download buttons
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/refactor-animate-js.yml b/changelogs/unreleased/refactor-animate-js.yml
new file mode 100644
index 00000000000..ec32d68bbdd
--- /dev/null
+++ b/changelogs/unreleased/refactor-animate-js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove animate.js and label animation.
+merge_request:
+author:
+type: removed
diff --git a/changelogs/unreleased/replace_project_archived-feature.yml b/changelogs/unreleased/replace_project_archived-feature.yml
new file mode 100644
index 00000000000..d0697347aa0
--- /dev/null
+++ b/changelogs/unreleased/replace_project_archived-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/archived.feature' spinach test with an rspec analog
+merge_request: 14322
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_project_commits_revert-feature.yml b/changelogs/unreleased/replace_project_commits_revert-feature.yml
new file mode 100644
index 00000000000..7fc9fcf3580
--- /dev/null
+++ b/changelogs/unreleased/replace_project_commits_revert-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/commits/revert.feature' spinach test with an rspec analog
+merge_request: 14325
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_project_snippets-feature.yml b/changelogs/unreleased/replace_project_snippets-feature.yml
new file mode 100644
index 00000000000..4fdee70008a
--- /dev/null
+++ b/changelogs/unreleased/replace_project_snippets-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/snippets.feature' spinach test with an rspec analog
+merge_request: 14326
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_search-feature.yml b/changelogs/unreleased/replace_search-feature.yml
new file mode 100644
index 00000000000..487f602ba30
--- /dev/null
+++ b/changelogs/unreleased/replace_search-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'search.feature' spinach test with an rspec analog
+merge_request: 14248
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/sh-optimize-discussion-json.yml b/changelogs/unreleased/sh-optimize-discussion-json.yml
new file mode 100644
index 00000000000..4be1bc89a91
--- /dev/null
+++ b/changelogs/unreleased/sh-optimize-discussion-json.yml
@@ -0,0 +1,5 @@
+---
+title: Eliminate N+1 queries in loading discussions.json endpoint
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml b/changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml
new file mode 100644
index 00000000000..34aa3d0db6f
--- /dev/null
+++ b/changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml
@@ -0,0 +1,5 @@
+---
+title: Made the "remember me" check boxes have consistent styles and alignment
+merge_request:
+author: Jedidiah Broadbent
+type: fixed
diff --git a/changelogs/unreleased/winh-protected-branch-modal-merged.yml b/changelogs/unreleased/winh-protected-branch-modal-merged.yml
new file mode 100644
index 00000000000..63f1f424a5d
--- /dev/null
+++ b/changelogs/unreleased/winh-protected-branch-modal-merged.yml
@@ -0,0 +1,5 @@
+---
+title: Display whether branch has been merged when deleting protected branch
+merge_request: 14220
+author:
+type: changed
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index d6c3c84851b..6c5c8cad270 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -416,3 +416,39 @@
:why: https://gitlab.com/gitlab-com/organization/issues/117
:versions: []
:when: 2017-09-04 12:59:51.150798717 Z
+- - :approve
+ - console-browserify
+ - :who: Mike Greiling
+ :why: https://github.com/Raynos/console-browserify/blob/f0a8898487e2a47b8a5dc8734b91059fa2825506/LICENCE
+ :versions: []
+ :when: 2017-09-16 05:13:07.073651000 Z
+- - :approve
+ - duplexer
+ - :who: Mike Greiling
+ :why: https://github.com/Raynos/duplexer/blob/master/LICENCE
+ :versions: []
+ :when: 2017-09-16 05:14:15.774643000 Z
+- - :approve
+ - json3
+ - :who: Mike Greiling
+ :why: https://github.com/bestiejs/json3/blob/v3.3.2/LICENSE
+ :versions: []
+ :when: 2017-09-16 05:15:16.273892000 Z
+- - :approve
+ - mime
+ - :who: Mike Greiling
+ :why: https://github.com/broofa/node-mime/blob/v1.3.4/LICENSE
+ :versions: []
+ :when: 2017-09-16 05:16:21.135542000 Z
+- - :approve
+ - querystring-es3
+ - :who: Mike Greiling
+ :why: https://github.com/mike-spainhower/querystring/blob/v0.2.0/License.md
+ :versions: []
+ :when: 2017-09-16 05:17:20.372089000 Z
+- - :approve
+ - utils-merge
+ - :who: Mike Greiling
+ :why: https://github.com/jaredhanson/utils-merge/blob/v1.0.0/LICENSE
+ :versions: []
+ :when: 2017-09-16 05:18:26.193764000 Z
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 40e635bf2cf..b89f0419b91 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -58,7 +58,7 @@ Doorkeeper.configure do
# For more information go to
# https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
default_scopes(*Gitlab::Auth::DEFAULT_SCOPES)
- optional_scopes(*Gitlab::Auth::OPTIONAL_SCOPES)
+ optional_scopes(*Gitlab::Auth.optional_scopes)
# Change the way client credentials are retrieved from the request object.
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb
index 943e01f1496..d3a7a2b9f8b 100644
--- a/config/initializers/static_files.rb
+++ b/config/initializers/static_files.rb
@@ -30,7 +30,7 @@ if app.config.serve_static_files
settings.merge!(
host: Gitlab.config.gitlab.host,
port: Gitlab.config.gitlab.port,
- https: Gitlab.config.gitlab.https
+ https: false
)
app.config.middleware.insert_before(
Gitlab::Middleware::Static,
diff --git a/db/migrate/20170828135939_migrate_user_external_mail_data.rb b/db/migrate/20170828135939_migrate_user_external_mail_data.rb
new file mode 100644
index 00000000000..592e141b7e6
--- /dev/null
+++ b/db/migrate/20170828135939_migrate_user_external_mail_data.rb
@@ -0,0 +1,57 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateUserExternalMailData < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
+
+ include EachBatch
+ end
+
+ class UserSyncedAttributesMetadata < ActiveRecord::Base
+ self.table_name = 'user_synced_attributes_metadata'
+
+ include EachBatch
+ end
+
+ def up
+ User.each_batch do |batch|
+ start_id, end_id = batch.pluck('MIN(id), MAX(id)').first
+
+ execute <<-EOF
+ INSERT INTO user_synced_attributes_metadata (user_id, provider, email_synced)
+ SELECT id, email_provider, external_email
+ FROM users
+ WHERE external_email = TRUE
+ AND NOT EXISTS (
+ SELECT true
+ FROM user_synced_attributes_metadata
+ WHERE user_id = users.id
+ AND provider = users.email_provider
+ )
+ AND id BETWEEN #{start_id} AND #{end_id}
+ EOF
+ end
+ end
+
+ def down
+ UserSyncedAttributesMetadata.each_batch do |batch|
+ start_id, end_id = batch.pluck('MIN(id), MAX(id)').first
+
+ execute <<-EOF
+ UPDATE users
+ SET users.email_provider = metadata.provider, users.external_email = metadata.email_synced
+ FROM user_synced_attributes_metadata as metadata, users
+ WHERE metadata.email_synced = TRUE
+ AND metadata.user_id = users.id
+ AND id BETWEEN #{start_id} AND #{end_id}
+ EOF
+ end
+ end
+end
diff --git a/db/migrate/20170913131410_environments_project_id_not_null.rb b/db/migrate/20170913131410_environments_project_id_not_null.rb
new file mode 100644
index 00000000000..d5404f8ede9
--- /dev/null
+++ b/db/migrate/20170913131410_environments_project_id_not_null.rb
@@ -0,0 +1,16 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class EnvironmentsProjectIdNotNull < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ change_column_null :environments, :project_id, false
+ end
+
+ def down
+ change_column_null :environments, :project_id, true
+ end
+end
diff --git a/db/migrate/20170914135630_add_index_for_recent_push_events.rb b/db/migrate/20170914135630_add_index_for_recent_push_events.rb
new file mode 100644
index 00000000000..99f593b0465
--- /dev/null
+++ b/db/migrate/20170914135630_add_index_for_recent_push_events.rb
@@ -0,0 +1,40 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexForRecentPushEvents < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index_if_not_present(
+ :merge_requests,
+ [:source_project_id, :source_branch]
+ )
+
+ remove_concurrent_index_if_present(:merge_requests, :source_project_id)
+ end
+
+ def down
+ add_concurrent_index_if_not_present(:merge_requests, :source_project_id)
+
+ remove_concurrent_index_if_present(
+ :merge_requests,
+ [:source_project_id, :source_branch]
+ )
+ end
+
+ def add_concurrent_index_if_not_present(table, columns)
+ return if index_exists?(table, columns)
+
+ add_concurrent_index(table, columns)
+ end
+
+ def remove_concurrent_index_if_present(table, columns)
+ return unless index_exists?(table, columns)
+
+ remove_concurrent_index(table, columns)
+ end
+end
diff --git a/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb b/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb
new file mode 100644
index 00000000000..3e84b295be4
--- /dev/null
+++ b/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb
@@ -0,0 +1,37 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class DeleteConflictingRedirectRoutes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'DeleteConflictingRedirectRoutesRange'.freeze
+ BATCH_SIZE = 200 # At 200, I expect under 20s per batch, which is under our query timeout of 60s.
+ DELAY_INTERVAL = 12.seconds
+
+ disable_ddl_transaction!
+
+ class Route < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'routes'
+ end
+
+ def up
+ say opening_message
+
+ queue_background_migration_jobs_by_range_at_intervals(Route, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # nothing
+ end
+
+ def opening_message
+ <<~MSG
+ Clean up redirect routes that conflict with regular routes.
+ See initial bug fix:
+ https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13357
+ MSG
+ end
+end
diff --git a/db/post_migrate/20170913180600_fix_projects_without_project_feature.rb b/db/post_migrate/20170913180600_fix_projects_without_project_feature.rb
new file mode 100644
index 00000000000..bfa9ad80c7d
--- /dev/null
+++ b/db/post_migrate/20170913180600_fix_projects_without_project_feature.rb
@@ -0,0 +1,33 @@
+class FixProjectsWithoutProjectFeature < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ # Deletes corrupted project features
+ sql = "DELETE FROM project_features WHERE project_id IS NULL"
+ execute(sql)
+
+ # Creates missing project features with private visibility
+ sql =
+ %Q{
+ INSERT INTO project_features(project_id, repository_access_level, issues_access_level, merge_requests_access_level, wiki_access_level,
+ builds_access_level, snippets_access_level, created_at, updated_at)
+ SELECT projects.id as project_id,
+ 10 as repository_access_level,
+ 10 as issues_access_level,
+ 10 as merge_requests_access_level,
+ 10 as wiki_access_level,
+ 10 as builds_access_level ,
+ 10 as snippets_access_level,
+ projects.created_at,
+ projects.updated_at
+ FROM projects
+ LEFT OUTER JOIN project_features ON project_features.project_id = projects.id
+ WHERE (project_features.id IS NULL)
+ }
+
+ execute(sql)
+ end
+
+ def down
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 2149f5ad23d..2d8c33591f0 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: 20170905112933) do
+ActiveRecord::Schema.define(version: 20170914135630) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -520,7 +520,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
create_table "environments", force: :cascade do |t|
- t.integer "project_id"
+ t.integer "project_id", null: false
t.string "name", null: false
t.datetime "created_at"
t.datetime "updated_at"
@@ -892,7 +892,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree
add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
- add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree
+ add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 5732b6a1ca4..40099dcc967 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -145,8 +145,8 @@ Omnibus installations:
```ruby
# /etc/gitlab/gitlab.rb
git_data_dirs({
- { 'default' => { 'path' => '/mnt/gitlab/default', 'gitaly_address' => 'tcp://gitlab.internal:9999' } },
- { 'storage1' => { 'path' => '/mnt/gitlab/storage1', 'gitaly_address' => 'tcp://gitlab.internal:9999' } },
+ 'default' => { 'path' => '/mnt/gitlab/default', 'gitaly_address' => 'tcp://gitlab.internal:9999' },
+ 'storage1' => { 'path' => '/mnt/gitlab/storage1', 'gitaly_address' => 'tcp://gitlab.internal:9999' },
})
gitlab_rails['gitaly_token'] = 'abc123secret'
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
index e99a7ee29cc..1304476e678 100644
--- a/doc/administration/reply_by_email.md
+++ b/doc/administration/reply_by_email.md
@@ -77,6 +77,33 @@ and use [an application password](https://support.google.com/mail/answer/185833)
To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
[Postfix setup documentation](reply_by_email_postfix_setup.md).
+### Security Concerns
+
+**WARNING:** Be careful when choosing the domain used for receiving incoming
+email.
+
+For the sake of example, suppose your top-level company domain is `hooli.com`.
+All employees in your company have an email address at that domain via Google
+Apps, and your company's private Slack instance requires a valid `@hooli.com`
+email address in order to sign up.
+
+If you also host a public-facing GitLab instance at `hooli.com` and set your
+incoming email domain to `hooli.com`, an attacker could abuse the "Create new
+issue by email" feature by using a project's unique address as the email when
+signing up for Slack, which would send a confirmation email, which would create
+a new issue on the project owned by the attacker, allowing them to click the
+confirmation link and validate their account on your company's private Slack
+instance.
+
+We recommend receiving incoming email on a subdomain, such as
+`incoming.hooli.com`, and ensuring that you do not employ any services that
+authenticate solely based on access to an email domain such as `*.hooli.com.`
+Alternatively, use a dedicated domain for GitLab email communications such as
+`hooli-gitlab.com`.
+
+See GitLab issue [#30366](https://gitlab.com/gitlab-org/gitlab-ce/issues/30366)
+for a real-world example of this exploit.
+
### Omnibus package installations
1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
@@ -141,7 +168,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
# The IDLE command timeout.
gitlab_rails['incoming_email_idle_timeout'] = 60
```
-
+
```ruby
# Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com
gitlab_rails['incoming_email_enabled'] = true
@@ -253,7 +280,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
# The IDLE command timeout.
idle_timeout: 60
```
-
+
```yaml
# Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com
incoming_email:
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 32fe5eea692..bebe6536b6e 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -131,7 +131,7 @@ Parameters:
"message": null
}
```
-The message will be `nil` when creating a lightweight tag otherwise
+The message will be `null` when creating a lightweight tag otherwise
it will contain the annotation.
In case of an error,
diff --git a/doc/api/wikis.md b/doc/api/wikis.md
index 10eebc4a3c1..15ce5f96b60 100644
--- a/doc/api/wikis.md
+++ b/doc/api/wikis.md
@@ -155,3 +155,5 @@ curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gi
```
On success the HTTP status code is `204` and no JSON response is expected.
+
+[ce-13372]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13372
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index c1362b7bd5b..acd5682841a 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -240,55 +240,18 @@ Remember that if your environment's name is `production` (all lowercase), then
it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md).
Double the benefit!
-## Web terminals
-
->**Note:**
-Web terminals were added in GitLab 8.15 and are only available to project
-masters and owners.
-
-If you deploy to your environments with the help of a deployment service (e.g.,
-the [Kubernetes service][kubernetes-service], GitLab can open
-a terminal session to your environment! This is a very powerful feature that
-allows you to debug issues without leaving the comfort of your web browser. To
-enable it, just follow the instructions given in the service documentation.
-
-Once enabled, your environments will gain a "terminal" button:
-
-![Terminal button on environment index](img/environments_terminal_button_on_index.png)
-
-You can also access the terminal button from the page for a specific environment:
-
-![Terminal button for an environment](img/environments_terminal_button_on_show.png)
-
-Wherever you find it, clicking the button will take you to a separate page to
-establish the terminal session:
-
-![Terminal page](img/environments_terminal_page.png)
-
-This works just like any other terminal - you'll be in the container created
-by your deployment, so you can run shell commands and get responses in real
-time, check the logs, try out configuration or code tweaks, etc. You can open
-multiple terminals to the same environment - they each get their own shell
-session - and even a multiplexer like `screen` or `tmux`!
-
->**Note:**
-Container-based deployments often lack basic tools (like an editor), and may
-be stopped or restarted at any time. If this happens, you will lose all your
-changes! Treat this as a debugging tool, not a comprehensive online IDE.
-
----
-
-While this is fine for deploying to some stable environments like staging or
-production, what happens for branches? So far we haven't defined anything
-regarding deployments for branches other than `master`. Dynamic environments
-will help us achieve that.
-
## Dynamic environments
As the name suggests, it is possible to create environments on the fly by just
declaring their names dynamically in `.gitlab-ci.yml`. Dynamic environments is
the basis of [Review apps](review_apps/index.md).
+>**Note:**
+The `name` and `url` parameters can use any of the defined CI variables,
+including predefined, secure variables and `.gitlab-ci.yml`
+[`variables`](yaml/README.md#variables).
+You however cannot use variables defined under `script` or on the Runner's side.
+
GitLab Runner exposes various [environment variables][variables] when a job runs,
and as such, you can use them as environment names. Let's add another job in
our example which will deploy to all branches except `master`:
@@ -434,7 +397,8 @@ Let's briefly see where URL that's defined in the environments is exposed.
## Making use of the environment URL
-The environment URL is exposed in a few places within GitLab.
+The [environment URL](yaml/README.md#environments-url) is exposed in a few
+places within GitLab.
| In a merge request widget as a link | In the Environments view as a button | In the Deployments view as a button |
| -------------------- | ------------ | ----------- |
@@ -598,7 +562,7 @@ exist, you should see something like:
>**Notes:**
>
-- For the monitor dashboard to appear, you need to:
+- For the monitoring dashboard to appear, you need to:
- Have enabled the [Prometheus integration][prom]
- Configured Prometheus to collect at least one [supported metric](../user/project/integrations/prometheus_library/metrics.md)
- With GitLab 9.2, all deployments to an environment are shown directly on the
@@ -608,8 +572,7 @@ If you have enabled [Prometheus for monitoring system and response metrics](http
Once configured, GitLab will attempt to retrieve [supported performance metrics](https://docs.gitlab.com/ee/user/project/integrations/prometheus_library/metrics.html) for any
environment which has had a successful deployment. If monitoring data was
-successfully retrieved, a Monitoring button will appear on the environment's
-detail page.
+successfully retrieved, a Monitoring button will appear for each environment.
![Environment Detail with Metrics](img/prometheus_environment_detail_with_metrics.png)
@@ -623,6 +586,49 @@ version of the app, all without leaving GitLab.
![Monitoring dashboard](img/environments_monitoring.png)
+## Web terminals
+
+>**Note:**
+Web terminals were added in GitLab 8.15 and are only available to project
+masters and owners.
+
+If you deploy to your environments with the help of a deployment service (e.g.,
+the [Kubernetes service][kubernetes-service], GitLab can open
+a terminal session to your environment! This is a very powerful feature that
+allows you to debug issues without leaving the comfort of your web browser. To
+enable it, just follow the instructions given in the service documentation.
+
+Once enabled, your environments will gain a "terminal" button:
+
+![Terminal button on environment index](img/environments_terminal_button_on_index.png)
+
+You can also access the terminal button from the page for a specific environment:
+
+![Terminal button for an environment](img/environments_terminal_button_on_show.png)
+
+Wherever you find it, clicking the button will take you to a separate page to
+establish the terminal session:
+
+![Terminal page](img/environments_terminal_page.png)
+
+This works just like any other terminal - you'll be in the container created
+by your deployment, so you can run shell commands and get responses in real
+time, check the logs, try out configuration or code tweaks, etc. You can open
+multiple terminals to the same environment - they each get their own shell
+session - and even a multiplexer like `screen` or `tmux`!
+
+>**Note:**
+Container-based deployments often lack basic tools (like an editor), and may
+be stopped or restarted at any time. If this happens, you will lose all your
+changes! Treat this as a debugging tool, not a comprehensive online IDE.
+
+---
+
+While this is fine for deploying to some stable environments like staging or
+production, what happens for branches? So far we haven't defined anything
+regarding deployments for branches other than `master`. Dynamic environments
+will help us achieve that.
+
## Checkout deployments locally
Since 8.13, a reference in the git repository is saved for each deployment, so
diff --git a/doc/ci/img/environments_monitoring.png b/doc/ci/img/environments_monitoring.png
index d9c46ea4c95..dcffdd1fdb8 100644
--- a/doc/ci/img/environments_monitoring.png
+++ b/doc/ci/img/environments_monitoring.png
Binary files differ
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 88e53ff40e8..2d56b2540ef 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -106,7 +106,7 @@ What is important is that each job is run independently from each other.
If you want to check whether your `.gitlab-ci.yml` file is valid, there is a
Lint tool under the page `/ci/lint` of your GitLab instance. You can also find
-a "CI Lint" button to go to this page under **Pipelines âž” Pipelines** and
+a "CI Lint" button to go to this page under **CI/CD âž” Pipelines** and
**Pipelines âž” Jobs** in your project.
For more information and a complete `.gitlab-ci.yml` syntax, please read
@@ -155,7 +155,7 @@ Find more information about different Runners in the
[Runners](../runners/README.md) documentation.
You can find whether any Runners are assigned to your project by going to
-**Settings âž” Pipelines**. Setting up a Runner is easy and straightforward. The
+**Settings âž” CI/CD**. Setting up a Runner is easy and straightforward. The
official Runner supported by GitLab is written in Go and its documentation
can be found at <https://docs.gitlab.com/runner/>.
@@ -168,7 +168,7 @@ Follow the links above to set up your own Runner or use a Shared Runner as
described in the next section.
Once the Runner has been set up, you should see it on the Runners page of your
-project, following **Settings âž” Pipelines**.
+project, following **Settings âž” CI/CD**.
![Activated runners](img/runners_activated.png)
@@ -181,7 +181,7 @@ These are special virtual machines that run on GitLab's infrastructure and can
build any project.
To enable the **Shared Runners** you have to go to your project's
-**Settings âž” Pipelines** and click **Enable shared runners**.
+**Settings âž” CI/CD** and click **Enable shared runners**.
[Read more on Shared Runners](../runners/README.md).
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index bac8e972754..8b51d112a2c 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -35,7 +35,7 @@ are:
A Runner that is specific only runs for the specified project(s). A shared Runner
can run jobs for every project that has enabled the option **Allow shared Runners**
-under **Settings âž” Pipelines**.
+under **Settings âž” CI/CD**.
Projects with high demand of CI activity can also benefit from using specific
Runners. By having dedicated Runners you are guaranteed that the Runner is not
@@ -61,7 +61,7 @@ You can only register a shared Runner if you are an admin of the GitLab instance
Shared Runners are enabled by default as of GitLab 8.2, but can be disabled
with the **Disable shared Runners** button which is present under each project's
-**Settings âž” Pipelines** page. Previous versions of GitLab defaulted shared
+**Settings âž” CI/CD** page. Previous versions of GitLab defaulted shared
Runners to disabled.
## Registering a specific Runner
@@ -76,7 +76,7 @@ Registering a specific can be done in two ways:
To create a specific Runner without having admin rights to the GitLab instance,
visit the project you want to make the Runner work for in GitLab:
-1. Go to **Settings âž” Pipelines** to obtain the token
+1. Go to **Settings âž” CI/CD** to obtain the token
1. [Register the Runner][register]
### Making an existing shared Runner specific
@@ -101,7 +101,7 @@ can be changed afterwards under each Runner's settings.
To lock/unlock a Runner:
-1. Visit your project's **Settings âž” Pipelines**
+1. Visit your project's **Settings âž” CI/CD**
1. Find the Runner you wish to lock/unlock and make sure it's enabled
1. Click the pencil button
1. Check the **Lock to current projects** option
@@ -115,7 +115,7 @@ you can enable the Runner also on any other project where you have Master permis
To enable/disable a Runner in your project:
-1. Visit your project's **Settings âž” Pipelines**
+1. Visit your project's **Settings âž” CI/CD**
1. Find the Runner you wish to enable/disable
1. Click **Enable for this project** or **Disable for this project**
@@ -136,7 +136,7 @@ Whenever a Runner is protected, the Runner picks only jobs created on
To protect/unprotect Runners:
-1. Visit your project's **Settings âž” Pipelines**
+1. Visit your project's **Settings âž” CI/CD**
1. Find a Runner you want to protect/unprotect and make sure it's enabled
1. Click the pencil button besides the Runner name
1. Check the **Protected** option
@@ -220,7 +220,7 @@ each Runner's settings.
To make a Runner pick tagged/untagged jobs:
-1. Visit your project's **Settings âž” Pipelines**
+1. Visit your project's **Settings âž” CI/CD**
1. Find the Runner you wish and make sure it's enabled
1. Click the pencil button
1. Check the **Run untagged jobs** option
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index cdb9858e179..e5a2bbd1773 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -34,7 +34,7 @@ instructions to [generate an SSH key](../../ssh/README.md). Do not add a
passphrase to the SSH key, or the `before_script` will prompt for it.
Then, create a new **Secret Variable** in your project settings on GitLab
-following **Settings > Pipelines** and look for the "Secret Variables" section.
+following **Settings > CI/CD** and look for the "Secret Variables" section.
As **Key** add the name `SSH_PRIVATE_KEY` and in the **Value** field paste the
content of your _private_ key that you created earlier.
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 7ec7136d8c6..56a16f77e7f 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -19,7 +19,7 @@ A unique trigger token can be obtained when [adding a new trigger](#adding-a-new
## Adding a new trigger
You can add a new trigger by going to your project's
-**Settings âž” Pipelines** under **Triggers**. The **Add trigger** button will
+**Settings âž” CI/CD** under **Triggers**. The **Add trigger** button will
create a new token which you can then use to trigger a rerun of this
particular project's pipeline.
@@ -43,7 +43,7 @@ From now on the trigger will be run as you.
## Revoking a trigger
You can revoke a trigger any time by going at your project's
-**Settings âž” Pipelines** under **Triggers** and hitting the **Revoke** button.
+**Settings âž” CI/CD** under **Triggers** and hitting the **Revoke** button.
The action is irreversible.
## Triggering a pipeline
@@ -64,7 +64,7 @@ POST /projects/:id/trigger/pipeline
The required parameters are the [trigger's `token`](#authentication-tokens)
and the Git `ref` on which the trigger will be performed. Valid refs are the
branch and the tag. The `:id` of a project can be found by
-[querying the API](../../api/projects.md) or by visiting the **Pipelines**
+[querying the API](../../api/projects.md) or by visiting the **CI/CD**
settings page which provides self-explanatory examples.
When a rerun of a pipeline is triggered, the information is exposed in GitLab's
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 6513b31826a..ebcb92b5db1 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -158,17 +158,17 @@ script:
settings. Follow the discussion in issue [#13784][ce-13784] for masking the
secret variables.
-GitLab CI allows you to define per-project or per-group **secret variables**
-that are set in the build environment. The secret variables are stored out of
-the repository (`.gitlab-ci.yml`) and are securely passed to GitLab Runner
-making them available in the build environment. It's the recommended method to
-use for storing things like passwords, secret keys and credentials.
+GitLab CI allows you to define per-project or per-group secret variables
+that are set in the pipeline environment. The secret variables are stored out of
+the repository (not in `.gitlab-ci.yml`) and are securely passed to GitLab Runner
+making them available during a pipeline run. It's the recommended method to
+use for storing things like passwords, SSH keys and credentials.
Project-level secret variables can be added by going to your project's
-**Settings âž” Pipelines**, then finding the section called **Secret variables**.
+**Settings > CI/CD**, then finding the section called **Secret variables**.
Likewise, group-level secret variables can be added by going to your group's
-**Settings âž” Pipelines**, then finding the section called **Secret variables**.
+**Settings > CI/CD**, then finding the section called **Secret variables**.
Any variables of [subgroups] will be inherited recursively.
Once you set them, they will be available for all subsequent pipelines. You can also
@@ -185,8 +185,8 @@ protected, it would only be securely passed to pipelines running on the
protected variables.
Protected variables can be added by going to your project's
-**Settings âž” Pipelines**, then finding the section called
-**Secret variables**, and check *Protected*.
+**Settings > CI/CD**, then finding the section called
+**Secret variables**, and check "Protected".
Once you set them, they will be available for all subsequent pipelines.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 78733b9cc4b..f69d71a5c39 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -727,6 +727,9 @@ deployment to the `production` environment.
- Before GitLab 8.11, the name of an environment could be defined as a string like
`environment: production`. The recommended way now is to define it under the
`name` keyword.
+- The `name` parameter can use any of the defined CI variables,
+ including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables).
+ You however cannot use variables defined under `script`.
The `environment` name can contain:
@@ -762,6 +765,9 @@ deploy to production:
- Introduced in GitLab 8.11.
- Before GitLab 8.11, the URL could be added only in GitLab's UI. The
recommended way now is to define it in `.gitlab-ci.yml`.
+- The `url` parameter can use any of the defined CI variables,
+ including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables).
+ You however cannot use variables defined under `script`.
This is an optional value that when set, it exposes buttons in various places
in GitLab which when clicked take you to the defined URL.
@@ -841,10 +847,9 @@ The `stop_review_app` job is **required** to have the following keywords defined
**Notes:**
- [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6.
- The `$CI_ENVIRONMENT_SLUG` was [introduced][ce-7983] in GitLab 8.15.
-
-`environment` can also represent a configuration hash with `name` and `url`.
-These parameters can use any of the defined [CI variables](#variables)
-(including predefined, secure variables and `.gitlab-ci.yml` variables).
+- The `name` and `url` parameters can use any of the defined CI variables,
+ including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables).
+ You however cannot use variables defined under `script`.
For example:
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 0c17905aa8c..44ee994a26b 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -253,7 +253,7 @@ only.
[^1]: On public and internal projects, all users are able to perform this action.
[^2]: Guest users can only view the confidential issues they created themselves
-[^3]: If **Public pipelines** is enabled in **Project Settings > Pipelines**
+[^3]: If **Public pipelines** is enabled in **Project Settings > CI/CD**
[^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner
[^5]: Only if user is not external one.
[^6]: Only if user is a member of the project.
diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
index 2a37cbd160b..17a47cfa646 100644
--- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md
+++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
@@ -11,7 +11,7 @@ GitLab has support for automatically detecting and monitoring the Kubernetes NGI
| Latency (ms) | avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}) |
| HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) |
-## Configuring Prometheus to monitor for NGINX ingress metrics
+## Configuring NGINX ingress monitoring
If you have deployed with the [gitlab-omnibus](https://docs.gitlab.com/ee/install/kubernetes/gitlab_omnibus.md) Helm chart, and your application is running in the same cluster, no further action is required. The ingress metrics will be automatically enabled and annotated for Prometheus monitoring. Simply ensure Prometheus monitoring is [enabled for your project](../prometheus.md), which is on by default.
@@ -20,14 +20,14 @@ For other deployments, there is some configuration required depending on your in
* NGINX Ingress should be annotated for Prometheus monitoring
* Prometheus should be configured to monitor annotated pods
-### Configuring NGINX Ingress for Prometheus monitoring
+### Setting up NGINX Ingress for Prometheus monitoring
Version 0.9.0 and above of [NGINX ingress](https://github.com/kubernetes/ingress/tree/master/controllers/nginx) have built-in support for exporting Prometheus metrics. To enable, a ConfigMap setting must be passed: `enable-vts-status: "true"`. Once enabled, a Prometheus metrics endpoint will start running on port 10254.
-With metric data now available, Prometheus needs to be configured to collect it. The easiest way to do this is to leverage Prometheus' [built-in Kubernetes service discovery](https://prometheus.io/docs/operating/configuration/#kubernetes_sd_config), which automatically detects a variety of Kubernetes components and makes them available for monitoring. NGINX ingress metrics are exposed per pod, a sample scrape configuration [is available](https://github.com/prometheus/prometheus/blob/master/documentation/examples/prometheus-kubernetes.yml#L248). This configuration will detect pods and enable collection of metrics **only if** they have been specifically annotated for monitoring.
+With metric data now available, Prometheus needs to be configured to collect it. The easiest way to do this is to leverage Prometheus' [built-in Kubernetes service discovery](https://prometheus.io/docs/operating/configuration/#kubernetes_sd_config), which automatically detects a variety of Kubernetes components and makes them available for monitoring. Since NGINX ingress metrics are exposed per pod, a scrape job for Kubernetes pods is required. A sample pod scraping configuration [is available](https://github.com/prometheus/prometheus/blob/master/documentation/examples/prometheus-kubernetes.yml#L248). This configuration will detect pods and enable collection of metrics **only if** they have been specifically annotated for monitoring.
Depending on how NGINX ingress was deployed, typically a DaemonSet or Deployment, edit the corresponding YML spec. Two new annotations need to be added:
-* `prometheus.io/port: "true"`
+* `prometheus.io/scrape: "true"`
* `prometheus.io/port: "10254"`
Prometheus should now be collecting NGINX ingress metrics. To validate view the Prometheus Targets, available under `Status > Targets` on the Prometheus dashboard. New entries for NGINX should be listed in the kubernetes pod monitoring job, `kubernetes-pods`.
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index dbc1305101f..56f58fd755a 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -1,7 +1,7 @@
# Pipelines settings
To reach the pipelines settings navigate to your project's
-**Settings âž” Pipelines**.
+**Settings âž” CI/CD**.
The following settings can be configured per project.
diff --git a/doc/user/project/repository/img/compare_branches.png b/doc/user/project/repository/img/compare_branches.png
index 353bd72ef4e..d7ab587f030 100755..100644
--- a/doc/user/project/repository/img/compare_branches.png
+++ b/doc/user/project/repository/img/compare_branches.png
Binary files differ
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index bcc3625f908..2b23c494dc4 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -31,8 +31,8 @@ on the search field on the top-right of your screen:
If you want to search for issues present in a specific project, navigate to
a project's **Issues** tab, and click on the field **Search or filter results...**. It will
-display a dropdown menu, from which you can add filters per author, assignee, milestone, label,
-and weight. When done, press **Enter** on your keyboard to filter the issues.
+display a dropdown menu, from which you can add filters per author, assignee, milestone,
+label, weight, and 'my-reaction' (based on your emoji votes). When done, press **Enter** on your keyboard to filter the issues.
![filter issues in a project](img/issue_search_filter.png)
diff --git a/features/project/archived.feature b/features/project/archived.feature
deleted file mode 100644
index ad466f4f307..00000000000
--- a/features/project/archived.feature
+++ /dev/null
@@ -1,30 +0,0 @@
-Feature: Project Archived
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And I own project "Forum"
-
- Scenario: I should not see archived on project page of not-archive project
- And project "Forum" is archived
- And I visit project "Shop" page
- Then I should not see "Archived"
-
- Scenario: I should see archived on project page of archive project
- And project "Forum" is archived
- And I visit project "Forum" page
- Then I should see "Archived"
-
- Scenario: I archive project
- When project "Shop" has push event
- And I visit project "Shop" page
- And I visit edit project "Shop" page
- And I set project archived
- Then I should see "Archived"
-
- Scenario: I unarchive project
- When project "Shop" has push event
- And project "Shop" is archived
- And I visit project "Shop" page
- And I visit edit project "Shop" page
- And I set project unarchived
- Then I should not see "Archived"
diff --git a/features/project/commits/revert.feature b/features/project/commits/revert.feature
deleted file mode 100644
index 7ee1d717d80..00000000000
--- a/features/project/commits/revert.feature
+++ /dev/null
@@ -1,31 +0,0 @@
-@project_commits
-Feature: Revert Commits
- Background:
- Given I sign in as a user
- And I own a project
- And I visit my project's commits page
-
- @javascript
- Scenario: I revert a commit
- Given I click on commit link
- And I click on the revert button
- And I revert the changes directly
- Then I should see the revert commit notice
-
- @javascript
- Scenario: I revert a commit that was previously reverted
- Given I click on commit link
- And I click on the revert button
- And I revert the changes directly
- And I visit my project's commits page
- And I click on commit link
- And I click on the revert button
- And I revert the changes directly
- Then I should see a revert error
-
- @javascript
- Scenario: I revert a commit in a new merge request
- Given I click on commit link
- And I click on the revert button
- And I revert the changes in a new merge request
- Then I should see the new merge request notice
diff --git a/features/project/snippets.feature b/features/project/snippets.feature
deleted file mode 100644
index 50bc4c93df3..00000000000
--- a/features/project/snippets.feature
+++ /dev/null
@@ -1,35 +0,0 @@
-Feature: Project Snippets
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" have "Snippet one" snippet
- And project "Shop" have no "Snippet two" snippet
- And I visit project "Shop" snippets page
-
- Scenario: I should see snippets
- Given I visit project "Shop" snippets page
- Then I should see "Snippet one" in snippets
- And I should not see "Snippet two" in snippets
-
- @javascript
- Scenario: I create new project snippet
- Given I click link "New snippet"
- And I submit new snippet "Snippet three"
- Then I should see snippet "Snippet three"
-
- @javascript
- Scenario: I comment on a snippet "Snippet one"
- Given I visit snippet page "Snippet one"
- And I leave a comment like "Good snippet!"
- Then I should see comment "Good snippet!"
-
- Scenario: I update "Snippet one"
- Given I visit snippet page "Snippet one"
- And I click link "Edit"
- And I submit new title "Snippet new title"
- Then I should see "Snippet new title"
-
- Scenario: I destroy "Snippet one"
- Given I visit snippet page "Snippet one"
- And I click link "Delete"
- Then I should not see "Snippet one" in snippets
diff --git a/features/search.feature b/features/search.feature
deleted file mode 100644
index f894b6b84a1..00000000000
--- a/features/search.feature
+++ /dev/null
@@ -1,100 +0,0 @@
-@dashboard
-Feature: Search
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And I visit dashboard search page
-
- Scenario: I should see project I am looking for
- Given I search for "Sho"
- Then I should see "Shop" project link
-
- @javascript
- Scenario: I should see issues I am looking for
- And project has issues
- When I search for "Foo"
- And I click "Issues" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see merge requests I am looking for
- And project has merge requests
- When I search for "Foo"
- When I click "Merge requests" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see milestones I am looking for
- And project has milestones
- When I search for "Foo"
- When I click "Milestones" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see project code I am looking for
- When I click project "Shop" link
- And I search for "rspec"
- Then I should see code results for project "Shop"
-
- @javascript
- Scenario: I should see project issues
- And project has issues
- When I click project "Shop" link
- And I search for "Foo"
- And I click "Issues" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see project merge requests
- And project has merge requests
- When I click project "Shop" link
- And I search for "Foo"
- And I click "Merge requests" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see project milestones
- And project has milestones
- When I click project "Shop" link
- And I search for "Foo"
- And I click "Milestones" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see Wiki blobs
- And project has Wiki content
- When I click project "Shop" link
- And I search for "Wiki content"
- And I click "Wiki" link
- Then I should see "test_wiki" link in the search results
-
- Scenario: I logout and should see project I am looking for
- Given project "Shop" is public
- And I logout directly
- And I visit dashboard search page
- And I search for "Sho"
- Then I should see "Shop" project link
-
- @javascript
- Scenario: I logout and should see issues I am looking for
- Given project "Shop" is public
- And I logout directly
- And I visit dashboard search page
- And project has issues
- When I search for "Foo"
- And I click "Issues" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- Scenario: I logout and should see project code I am looking for
- Given project "Shop" is public
- And I logout directly
- When I visit project "Shop" page
- And I search for "rspec" on project page
- Then I should see code results for project "Shop"
diff --git a/features/steps/project/archived.rb b/features/steps/project/archived.rb
deleted file mode 100644
index e4847180be9..00000000000
--- a/features/steps/project/archived.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-class Spinach::Features::ProjectArchived < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
-
- When 'project "Forum" is archived' do
- project = Project.find_by(name: "Forum")
- project.update_attribute(:archived, true)
- end
-
- When 'project "Shop" is archived' do
- project = Project.find_by(name: "Shop")
- project.update_attribute(:archived, true)
- end
-
- When 'I visit project "Forum" page' do
- project = Project.find_by(name: "Forum")
- visit project_path(project)
- end
-
- step 'I should not see "Archived"' do
- expect(page).not_to have_content "Archived"
- end
-
- step 'I should see "Archived"' do
- expect(page).to have_content "Archived"
- end
-
- When 'I set project archived' do
- click_link "Archive"
- end
-
- When 'I set project unarchived' do
- click_link "Unarchive"
- end
-end
diff --git a/features/steps/project/commits/revert.rb b/features/steps/project/commits/revert.rb
deleted file mode 100644
index ebfa7a878bb..00000000000
--- a/features/steps/project/commits/revert.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-class Spinach::Features::RevertCommits < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include SharedDiffNote
- include RepoHelpers
-
- step 'I click on commit link' do
- visit project_commit_path(@project, sample_commit.id)
- end
-
- step 'I click on the revert button' do
- find(".header-action-buttons .dropdown").click
- find("a[href='#modal-revert-commit']").click
- end
-
- step 'I revert the changes directly' do
- page.within('#modal-revert-commit') do
- uncheck 'create_merge_request'
- click_button 'Revert'
- end
- end
-
- step 'I should see the revert commit notice' do
- page.should have_content('The commit has been successfully reverted.')
- end
-
- step 'I should see a revert error' do
- page.should have_content('Sorry, we cannot revert this commit automatically.')
- end
-
- step 'I revert the changes in a new merge request' do
- page.within('#modal-revert-commit') do
- click_button 'Revert'
- end
- end
-
- step 'I should see the new merge request notice' do
- page.should have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
- page.should have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master")
- end
-end
diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
deleted file mode 100644
index 12257593621..00000000000
--- a/features/steps/project/snippets.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedNote
- include SharedPaths
- include WaitForRequests
-
- step 'project "Shop" have "Snippet one" snippet' do
- create(:project_snippet,
- title: "Snippet one",
- content: "Test content",
- file_name: "snippet.rb",
- project: project,
- author: project.users.first)
- end
-
- step 'project "Shop" have no "Snippet two" snippet' do
- create(:snippet,
- title: "Snippet two",
- content: "Test content",
- file_name: "snippet.rb",
- author: project.users.first)
- end
-
- step 'I click link "New snippet"' do
- page.within '.nav-controls' do
- first(:link, "New snippet").click
- end
- end
-
- step 'I click link "Snippet one"' do
- click_link "Snippet one"
- end
-
- step 'I should see "Snippet one" in snippets' do
- expect(page).to have_content "Snippet one"
- end
-
- step 'I should not see "Snippet two" in snippets' do
- expect(page).not_to have_content "Snippet two"
- end
-
- step 'I should not see "Snippet one" in snippets' do
- expect(page).not_to have_content "Snippet one"
- end
-
- step 'I click link "Edit"' do
- page.within ".detail-page-header" do
- first(:link, "Edit").click
- end
- end
-
- step 'I click link "Delete"' do
- first(:link, "Delete").click
- end
-
- step 'I submit new snippet "Snippet three"' do
- fill_in "project_snippet_title", with: "Snippet three"
- fill_in "project_snippet_file_name", with: "my_snippet.rb"
- page.within('.file-editor') do
- find('.ace_text-input').native.send_keys 'Content of snippet three'
- end
- click_button "Create snippet"
- wait_for_requests
- end
-
- step 'I should see snippet "Snippet three"' do
- expect(page).to have_content "Snippet three"
- expect(page).to have_content "Content of snippet three"
- end
-
- step 'I submit new title "Snippet new title"' do
- fill_in "project_snippet_title", with: "Snippet new title"
- click_button "Save"
- end
-
- step 'I should see "Snippet new title"' do
- expect(page).to have_content "Snippet new title"
- end
-
- step 'I leave a comment like "Good snippet!"' do
- page.within('.js-main-target-form') do
- fill_in "note_note", with: "Good snippet!"
- click_button "Comment"
- end
- wait_for_requests
- end
-
- step 'I should see comment "Good snippet!"' do
- expect(page).to have_content "Good snippet!"
- end
-
- step 'I visit snippet page "Snippet one"' do
- visit project_snippet_path(project, project_snippet)
- end
-
- def project_snippet
- @project_snippet ||= ProjectSnippet.find_by!(title: "Snippet one")
- end
-end
diff --git a/features/steps/search.rb b/features/steps/search.rb
deleted file mode 100644
index 2f1b8cc0156..00000000000
--- a/features/steps/search.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-class Spinach::Features::Search < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
-
- step 'I search for "Sho"' do
- fill_in "dashboard_search", with: "Sho"
- click_button "Search"
- end
-
- step 'I search for "Foo"' do
- fill_in "dashboard_search", with: "Foo"
- find('.btn-search').click
- end
-
- step 'I search for "rspec"' do
- fill_in "dashboard_search", with: "rspec"
- find('.btn-search').click
- end
-
- step 'I search for "rspec" on project page' do
- fill_in "search", with: "rspec"
- click_button "Go"
- end
-
- step 'I search for "Wiki content"' do
- fill_in "dashboard_search", with: "content"
- find('.btn-search').click
- end
-
- step 'I click "Issues" link' do
- page.within '.search-filter' do
- click_link 'Issues'
- end
- end
-
- step 'I click project "Shop" link' do
- find('.js-search-project-dropdown').click
- page.within '.project-filter' do
- click_link project.name_with_namespace
- end
- end
-
- step 'I click "Merge requests" link' do
- page.within '.search-filter' do
- click_link 'Merge requests'
- end
- end
-
- step 'I click "Milestones" link' do
- page.within '.search-filter' do
- click_link 'Milestones'
- end
- end
-
- step 'I click "Wiki" link' do
- page.within '.search-filter' do
- click_link 'Wiki'
- end
- end
-
- step 'I should see "Shop" project link' do
- expect(page).to have_link "Shop"
- end
-
- step 'I should see code results for project "Shop"' do
- page.within('.results') do
- page.should have_content 'Update capybara, rspec-rails, poltergeist to recent versions'
- end
- end
-
- step 'I search for "Contibuting"' do
- fill_in "dashboard_search", with: "Contibuting"
- click_button "Search"
- end
-
- step 'project has issues' do
- create(:issue, title: "Foo", project: project)
- create(:issue, title: "Bar", project: project)
- end
-
- step 'project has merge requests' do
- create(:merge_request, title: "Foo", source_project: project, target_project: project)
- create(:merge_request, :simple, title: "Bar", source_project: project, target_project: project)
- end
-
- step 'project has milestones' do
- create(:milestone, title: "Foo", project: project)
- create(:milestone, title: "Bar", project: project)
- end
-
- step 'I should see "Foo" link in the search results' do
- page.within('.results') do
- find(:css, '.search-results').should have_link 'Foo'
- end
- end
-
- step 'I should not see "Bar" link in the search results' do
- expect(find(:css, '.search-results')).not_to have_link 'Bar'
- end
-
- step 'I should see "test_wiki" link in the search results' do
- page.within('.results') do
- expect(find(:css, '.search-results')).to have_link 'test_wiki'
- end
- end
-
- step 'project has Wiki content' do
- @wiki = ::ProjectWiki.new(project, current_user)
- @wiki.create_page("test_wiki", "Some Wiki content", :markdown, "first commit")
- end
-
- step 'project "Shop" is public' do
- project.update_attributes(visibility_level: Project::PUBLIC)
- end
-end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 1825c90a23b..bdebda58d3f 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -88,7 +88,7 @@ module API
user = User.find_by(id: params[:id])
not_found!('User') unless user && can?(current_user, :read_user, user)
- opts = current_user&.admin? ? { with: Entities::UserWithAdmin } : {}
+ opts = current_user&.admin? ? { with: Entities::UserWithAdmin } : { with: Entities::User }
present user, opts
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 11ace83c15c..87aeb76b66a 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -2,7 +2,7 @@ module Gitlab
module Auth
MissingPersonalTokenError = Class.new(StandardError)
- REGISTRY_SCOPES = Gitlab.config.registry.enabled ? [:read_registry].freeze : [].freeze
+ REGISTRY_SCOPES = [:read_registry].freeze
# Scopes used for GitLab API access
API_SCOPES = [:api, :read_user].freeze
@@ -13,11 +13,6 @@ module Gitlab
# Default scopes for OAuth applications that don't define their own
DEFAULT_SCOPES = [:api].freeze
- AVAILABLE_SCOPES = (API_SCOPES + REGISTRY_SCOPES).freeze
-
- # Other available scopes
- OPTIONAL_SCOPES = (AVAILABLE_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze
-
class << self
include Gitlab::CurrentSettings
@@ -132,7 +127,7 @@ module Gitlab
token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password)
- if token && valid_scoped_token?(token, AVAILABLE_SCOPES)
+ if token && valid_scoped_token?(token, available_scopes)
Gitlab::Auth::Result.new(token.user, nil, :personal_token, abilities_for_scope(token.scopes))
end
end
@@ -230,6 +225,21 @@ module Gitlab
def read_user_scope_authentication_abilities
[]
end
+
+ def available_scopes
+ API_SCOPES + registry_scopes
+ end
+
+ # Other available scopes
+ def optional_scopes
+ available_scopes + OPENID_SCOPES - DEFAULT_SCOPES
+ end
+
+ def registry_scopes
+ return [] unless Gitlab.config.registry.enabled
+
+ REGISTRY_SCOPES
+ end
end
end
end
diff --git a/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb b/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb
new file mode 100644
index 00000000000..b1411be3016
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ module BackgroundMigration
+ class DeleteConflictingRedirectRoutesRange
+ class Route < ActiveRecord::Base
+ self.table_name = 'routes'
+ end
+
+ class RedirectRoute < ActiveRecord::Base
+ self.table_name = 'redirect_routes'
+ end
+
+ # start_id - The start ID of the range of events to process
+ # end_id - The end ID of the range to process.
+ def perform(start_id, end_id)
+ return unless migrate?
+
+ conflicts = RedirectRoute.where(routes_match_redirects_clause(start_id, end_id))
+ num_rows = conflicts.delete_all
+
+ Rails.logger.info("Gitlab::BackgroundMigration::DeleteConflictingRedirectRoutesRange [#{start_id}, #{end_id}] - Deleted #{num_rows} redirect routes that were conflicting with routes.")
+ end
+
+ def migrate?
+ Route.table_exists? && RedirectRoute.table_exists?
+ end
+
+ def routes_match_redirects_clause(start_id, end_id)
+ <<~ROUTES_MATCH_REDIRECTS
+ EXISTS (
+ SELECT 1 FROM routes
+ WHERE (
+ LOWER(redirect_routes.path) = LOWER(routes.path)
+ OR LOWER(redirect_routes.path) LIKE LOWER(CONCAT(routes.path, '/%'))
+ )
+ AND routes.id BETWEEN #{start_id} AND #{end_id}
+ )
+ ROUTES_MATCH_REDIRECTS
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index fb14798efe6..2c35da8f1aa 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -1,6 +1,9 @@
module Gitlab
module Database
module MigrationHelpers
+ BACKGROUND_MIGRATION_BATCH_SIZE = 1000 # Number of rows to process per job
+ BACKGROUND_MIGRATION_JOB_BUFFER_SIZE = 1000 # Number of jobs to bulk queue at a time
+
# Adds `created_at` and `updated_at` columns with timezone information.
#
# This method is an improved version of Rails' built-in method `add_timestamps`.
@@ -653,6 +656,91 @@ into similar problems in the future (e.g. when new tables are created).
EOF
end
end
+
+ # Bulk queues background migration jobs for an entire table, batched by ID range.
+ # "Bulk" meaning many jobs will be pushed at a time for efficiency.
+ # If you need a delay interval per job, then use `queue_background_migration_jobs_by_range_at_intervals`.
+ #
+ # model_class - The table being iterated over
+ # job_class_name - The background migration job class as a string
+ # batch_size - The maximum number of rows per job
+ #
+ # Example:
+ #
+ # class Route < ActiveRecord::Base
+ # include EachBatch
+ # self.table_name = 'routes'
+ # end
+ #
+ # bulk_queue_background_migration_jobs_by_range(Route, 'ProcessRoutes')
+ #
+ # Where the model_class includes EachBatch, and the background migration exists:
+ #
+ # class Gitlab::BackgroundMigration::ProcessRoutes
+ # def perform(start_id, end_id)
+ # # do something
+ # end
+ # end
+ def bulk_queue_background_migration_jobs_by_range(model_class, job_class_name, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE)
+ raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
+
+ jobs = []
+
+ model_class.each_batch(of: batch_size) do |relation|
+ start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
+
+ if jobs.length >= BACKGROUND_MIGRATION_JOB_BUFFER_SIZE
+ # Note: This code path generally only helps with many millions of rows
+ # We push multiple jobs at a time to reduce the time spent in
+ # Sidekiq/Redis operations. We're using this buffer based approach so we
+ # don't need to run additional queries for every range.
+ BackgroundMigrationWorker.perform_bulk(jobs)
+ jobs.clear
+ end
+
+ jobs << [job_class_name, [start_id, end_id]]
+ end
+
+ BackgroundMigrationWorker.perform_bulk(jobs) unless jobs.empty?
+ end
+
+ # Queues background migration jobs for an entire table, batched by ID range.
+ # Each job is scheduled with a `delay_interval` in between.
+ # If you use a small interval, then some jobs may run at the same time.
+ #
+ # model_class - The table being iterated over
+ # job_class_name - The background migration job class as a string
+ # delay_interval - The duration between each job's scheduled time (must respond to `to_f`)
+ # batch_size - The maximum number of rows per job
+ #
+ # Example:
+ #
+ # class Route < ActiveRecord::Base
+ # include EachBatch
+ # self.table_name = 'routes'
+ # end
+ #
+ # queue_background_migration_jobs_by_range_at_intervals(Route, 'ProcessRoutes', 1.minute)
+ #
+ # Where the model_class includes EachBatch, and the background migration exists:
+ #
+ # class Gitlab::BackgroundMigration::ProcessRoutes
+ # def perform(start_id, end_id)
+ # # do something
+ # end
+ # end
+ def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE)
+ raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
+
+ model_class.each_batch(of: batch_size) do |relation, index|
+ start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
+
+ # `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for
+ # the same time, which is not helpful in most cases where we wish to
+ # spread the work over time.
+ BackgroundMigrationWorker.perform_in(delay_interval * index, job_class_name, [start_id, end_id])
+ end
+ end
end
end
end
diff --git a/lib/gitlab/database/read_only_relation.rb b/lib/gitlab/database/read_only_relation.rb
new file mode 100644
index 00000000000..4571ad122ce
--- /dev/null
+++ b/lib/gitlab/database/read_only_relation.rb
@@ -0,0 +1,16 @@
+module Gitlab
+ module Database
+ # Module that can be injected into a ActiveRecord::Relation to make it
+ # read-only.
+ module ReadOnlyRelation
+ [:delete, :delete_all, :update, :update_all].each do |method|
+ define_method(method) do |*args|
+ raise(
+ ActiveRecord::ReadOnlyRecord,
+ "This relation is marked as read-only"
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb
index 919965100ae..010b4be7b40 100644
--- a/lib/gitlab/diff/inline_diff_marker.rb
+++ b/lib/gitlab/diff/inline_diff_marker.rb
@@ -2,9 +2,10 @@ module Gitlab
module Diff
class InlineDiffMarker < Gitlab::StringRangeMarker
def mark(line_inline_diffs, mode: nil)
- super(line_inline_diffs) do |text, left:, right:|
+ mark = super(line_inline_diffs) do |text, left:, right:|
%{<span class="#{html_class_names(left, right, mode)}">#{text}</span>}
end
+ mark.html_safe
end
private
diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb
index 9e6fca8c80c..dcdec818f5e 100644
--- a/lib/gitlab/git/operation_service.rb
+++ b/lib/gitlab/git/operation_service.rb
@@ -1,11 +1,18 @@
module Gitlab
module Git
class OperationService
- attr_reader :committer, :repository
+ WithBranchResult = Struct.new(:newrev, :repo_created, :branch_created) do
+ alias_method :repo_created?, :repo_created
+ alias_method :branch_created?, :branch_created
+ end
+
+ attr_reader :user, :repository
- def initialize(committer, new_repository)
- committer = Gitlab::Git::Committer.from_user(committer) if committer.is_a?(User)
- @committer = committer
+ def initialize(user, new_repository)
+ if user
+ user = Gitlab::Git::User.from_gitlab(user) unless user.respond_to?(:gl_id)
+ @user = user
+ end
# Refactoring aid
unless new_repository.is_a?(Gitlab::Git::Repository)
@@ -105,7 +112,7 @@ module Gitlab
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
update_ref_in_hooks(ref, newrev, oldrev)
- [newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev)]
+ WithBranchResult.new(newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev))
end
def find_oldrev_from_branch(newrev, branch)
@@ -128,7 +135,7 @@ module Gitlab
def with_hooks(ref, newrev, oldrev)
Gitlab::Git::HooksService.new.execute(
- committer,
+ user,
repository,
oldrev,
newrev,
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index efa13590a2c..c499ff101b5 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -610,49 +610,86 @@ module Gitlab
# TODO: implement this method
end
- def add_branch(branch_name, committer:, target:)
+ def add_branch(branch_name, user:, target:)
target_object = Ref.dereference_object(lookup(target))
raise InvalidRef.new("target not found: #{target}") unless target_object
- OperationService.new(committer, self).add_branch(branch_name, target_object.oid)
+ OperationService.new(user, self).add_branch(branch_name, target_object.oid)
find_branch(branch_name)
rescue Rugged::ReferenceError => ex
raise InvalidRef, ex
end
- def add_tag(tag_name, committer:, target:, message: nil)
+ def add_tag(tag_name, user:, target:, message: nil)
target_object = Ref.dereference_object(lookup(target))
raise InvalidRef.new("target not found: #{target}") unless target_object
- committer = Committer.from_user(committer) if committer.is_a?(User)
+ user = Gitlab::Git::User.from_gitlab(user) unless user.respond_to?(:gl_id)
options = nil # Use nil, not the empty hash. Rugged cares about this.
if message
options = {
message: message,
- tagger: Gitlab::Git.committer_hash(email: committer.email, name: committer.name)
+ tagger: Gitlab::Git.committer_hash(email: user.email, name: user.name)
}
end
- OperationService.new(committer, self).add_tag(tag_name, target_object.oid, options)
+ OperationService.new(user, self).add_tag(tag_name, target_object.oid, options)
find_tag(tag_name)
rescue Rugged::ReferenceError => ex
raise InvalidRef, ex
end
- def rm_branch(branch_name, committer:)
- OperationService.new(committer, self).rm_branch(find_branch(branch_name))
+ def rm_branch(branch_name, user:)
+ OperationService.new(user, self).rm_branch(find_branch(branch_name))
end
- def rm_tag(tag_name, committer:)
- OperationService.new(committer, self).rm_tag(find_tag(tag_name))
+ def rm_tag(tag_name, user:)
+ OperationService.new(user, self).rm_tag(find_tag(tag_name))
end
def find_tag(name)
tags.find { |tag| tag.name == name }
end
+ def merge(user, source_sha, target_branch, message)
+ committer = Gitlab::Git.committer_hash(email: user.email, name: user.name)
+
+ OperationService.new(user, self).with_branch(target_branch) do |start_commit|
+ our_commit = start_commit.sha
+ their_commit = source_sha
+
+ raise 'Invalid merge target' unless our_commit
+ raise 'Invalid merge source' unless their_commit
+
+ merge_index = rugged.merge_commits(our_commit, their_commit)
+ break if merge_index.conflicts?
+
+ options = {
+ parents: [our_commit, their_commit],
+ tree: merge_index.write_tree(rugged),
+ message: message,
+ author: committer,
+ committer: committer
+ }
+
+ commit_id = create_commit(options)
+
+ yield commit_id
+
+ commit_id
+ end
+ rescue Gitlab::Git::CommitError # when merge_index.conflicts?
+ nil
+ end
+
+ def create_commit(params = {})
+ params[:message].delete!("\r")
+
+ Rugged::Commit.create(rugged, params)
+ end
+
# Delete the specified branch from the repository
def delete_branch(branch_name)
gitaly_migrate(:delete_branch) do |is_enabled|
diff --git a/lib/gitlab/git/committer.rb b/lib/gitlab/git/user.rb
index 1f4bcf7a3a0..ea634d39668 100644
--- a/lib/gitlab/git/committer.rb
+++ b/lib/gitlab/git/user.rb
@@ -1,10 +1,14 @@
module Gitlab
module Git
- class Committer
+ class User
attr_reader :name, :email, :gl_id
- def self.from_user(user)
- new(user.name, user.email, Gitlab::GlId.gl_id(user))
+ def self.from_gitlab(gitlab_user)
+ new(gitlab_user.name, gitlab_user.email, Gitlab::GlId.gl_id(gitlab_user))
+ end
+
+ def self.from_gitaly(gitaly_user)
+ new(gitaly_user.name, gitaly_user.email, gitaly_user.gl_id)
end
def initialize(name, email, gl_id)
diff --git a/lib/gitlab/group_hierarchy.rb b/lib/gitlab/group_hierarchy.rb
index 5a31e56cb30..635f52131f9 100644
--- a/lib/gitlab/group_hierarchy.rb
+++ b/lib/gitlab/group_hierarchy.rb
@@ -22,7 +22,7 @@ module Gitlab
def base_and_ancestors
return ancestors_base unless Group.supports_nested_groups?
- base_and_ancestors_cte.apply_to(model.all)
+ read_only(base_and_ancestors_cte.apply_to(model.all))
end
# Returns a relation that includes the descendants_base set of groups
@@ -30,7 +30,7 @@ module Gitlab
def base_and_descendants
return descendants_base unless Group.supports_nested_groups?
- base_and_descendants_cte.apply_to(model.all)
+ read_only(base_and_descendants_cte.apply_to(model.all))
end
# Returns a relation that includes the base groups, their ancestors,
@@ -67,11 +67,13 @@ module Gitlab
union = SQL::Union.new([model.unscoped.from(ancestors_table),
model.unscoped.from(descendants_table)])
- model
+ relation = model
.unscoped
.with
.recursive(ancestors.to_arel, descendants.to_arel)
.from("(#{union.to_sql}) #{model.table_name}")
+
+ read_only(relation)
end
private
@@ -107,5 +109,12 @@ module Gitlab
def groups_table
model.arel_table
end
+
+ def read_only(relation)
+ # relations using a CTE are not safe to use with update_all as it will
+ # throw away the CTE, hence we mark them as read-only.
+ relation.extend(Gitlab::Database::ReadOnlyRelation)
+ relation
+ end
end
end
diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb
index 1f331b1e91d..5b5ed449f94 100644
--- a/lib/gitlab/o_auth/auth_hash.rb
+++ b/lib/gitlab/o_auth/auth_hash.rb
@@ -13,7 +13,7 @@ module Gitlab
end
def provider
- @provider ||= Gitlab::Utils.force_utf8(auth_hash.provider.to_s)
+ @provider ||= auth_hash.provider.to_s
end
def name
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index 703adae12cb..4e1ec1402ea 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -19,13 +19,12 @@ module Gitlab
end
def initialize(url, credentials: nil)
- @url = Addressable::URI.parse(url.to_s.strip)
-
%i[user password].each do |symbol|
credentials[symbol] = credentials[symbol].presence if credentials&.key?(symbol)
end
@credentials = credentials
+ @url = parse_url(url)
end
def sanitized_url
@@ -49,12 +48,30 @@ module Gitlab
private
+ def parse_url(url)
+ url = url.to_s.strip
+ match = url.match(%r{\A(?:git|ssh|http(?:s?))\://(?:(.+)(?:@))?(.+)})
+ raw_credentials = match[1] if match
+
+ if raw_credentials.present?
+ url.sub!("#{raw_credentials}@", '')
+
+ user, password = raw_credentials.split(':')
+ @credentials ||= { user: user.presence, password: password.presence }
+ end
+
+ url = Addressable::URI.parse(url)
+ url.password = password if password.present?
+ url.user = user if user.present?
+ url
+ end
+
def generate_full_url
return @url unless valid_credentials?
@full_url = @url.dup
- @full_url.password = credentials[:password]
- @full_url.user = credentials[:user]
+ @full_url.password = credentials[:password] if credentials[:password].present?
+ @full_url.user = credentials[:user] if credentials[:user].present?
@full_url
end
diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb
index dfa06c78d46..5163099cd98 100644
--- a/spec/controllers/boards/issues_controller_spec.rb
+++ b/spec/controllers/boards/issues_controller_spec.rb
@@ -45,6 +45,17 @@ describe Boards::IssuesController do
expect(parsed_response.length).to eq 2
expect(development.issues.map(&:relative_position)).not_to include(nil)
end
+
+ it 'avoids N+1 database queries' do
+ create(:labeled_issue, project: project, labels: [development])
+ control_count = ActiveRecord::QueryRecorder.new { list_issues(user: user, board: board, list: list2) }.count
+
+ # 25 issues is bigger than the page size
+ # the relative position will ignore the `#make_sure_position_set` queries
+ create_list(:labeled_issue, 25, project: project, labels: [development], assignees: [johndoe], relative_position: 1)
+
+ expect { list_issues(user: user, board: board, list: list2) }.not_to exceed_query_limit(control_count)
+ end
end
context 'with invalid list id' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 5d9403c23ac..b4a22a46b51 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -900,5 +900,37 @@ describe Projects::IssuesController do
expect(JSON.parse(response.body).first.keys).to match_array(%w[id reply_id expanded notes individual_note])
end
+
+ context 'with cross-reference system note', :request_store do
+ let(:new_issue) { create(:issue) }
+ let(:cross_reference) { "mentioned in #{new_issue.to_reference(issue.project)}" }
+
+ before do
+ create(:discussion_note_on_issue, :system, noteable: issue, project: issue.project, note: cross_reference)
+ end
+
+ it 'filters notes that the user should not see' do
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
+
+ expect(JSON.parse(response.body).count).to eq(1)
+ end
+
+ it 'does not result in N+1 queries' do
+ # Instantiate the controller variables to ensure QueryRecorder has an accurate base count
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
+
+ RequestStore.clear!
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
+ end.count
+
+ RequestStore.clear!
+
+ create_list(:discussion_note_on_issue, 2, :system, noteable: issue, project: issue.project, note: cross_reference)
+
+ expect { get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid }.not_to exceed_query_limit(control_count)
+ end
+ end
end
end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 2577d98df6f..7ce6a61d50c 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -25,7 +25,7 @@ feature 'Group merge requests page' do
end
it 'ignores archived merge request count badges in navbar' do
- expect( page.find('[aria-label="Merge Requests"] span.badge.count').text).to eq("1")
+ expect(first(:link, text: 'Merge Requests').find('.badge').text).to eq("1")
end
it 'ignores archived merge request count badges in state-filters' do
diff --git a/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb
new file mode 100644
index 00000000000..5ed4f3ad2bc
--- /dev/null
+++ b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+feature 'Groups > User sees users dropdowns in issuables list' do
+ let(:entity) { create(:group) }
+ let(:user_in_dropdown) { create(:user) }
+ let!(:user_not_in_dropdown) { create(:user) }
+ let!(:project) { create(:project, group: entity) }
+
+ before do
+ entity.add_developer(user_in_dropdown)
+ end
+
+ it_behaves_like 'issuable user dropdown behaviors' do
+ let(:issuable) { create(:issue, project: project) }
+ let(:issuables_path) { issues_group_path(entity) }
+ end
+
+ it_behaves_like 'issuable user dropdown behaviors' do
+ let(:issuable) { create(:merge_request, source_project: project) }
+ let(:issuables_path) { merge_requests_group_path(entity) }
+ end
+end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 9261acda9dc..7437c469a72 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -159,7 +159,7 @@ feature 'Issues > User uses quick actions', js: true do
describe 'move the issue to another project' do
let(:issue) { create(:issue, project: project) }
- context 'when the project is valid', js: true do
+ context 'when the project is valid' do
let(:target_project) { create(:project, :public) }
before do
@@ -180,7 +180,7 @@ feature 'Issues > User uses quick actions', js: true do
end
end
- context 'when the project is valid but the user not authorized', js: true do
+ context 'when the project is valid but the user not authorized' do
let(:project_unauthorized) {create(:project, :public)}
before do
@@ -196,7 +196,7 @@ feature 'Issues > User uses quick actions', js: true do
end
end
- context 'when the project is invalid', js: true do
+ context 'when the project is invalid' do
before do
sign_in(user)
visit project_issue_path(project, issue)
@@ -210,7 +210,7 @@ feature 'Issues > User uses quick actions', js: true do
end
end
- context 'when the user issues multiple commands', js: true do
+ context 'when the user issues multiple commands' do
let(:target_project) { create(:project, :public) }
let(:milestone) { create(:milestone, title: '1.0', project: project) }
let(:target_milestone) { create(:milestone, title: '1.0', project: target_project) }
diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb
index 624f13922ed..50c5e0bb65f 100644
--- a/spec/features/milestones/show_spec.rb
+++ b/spec/features/milestones/show_spec.rb
@@ -18,9 +18,9 @@ describe 'Milestone show' do
it 'avoids N+1 database queries' do
create(:labeled_issue, issue_params)
- control_count = ActiveRecord::QueryRecorder.new { visit_milestone }.count
+ control = ActiveRecord::QueryRecorder.new { visit_milestone }
create_list(:labeled_issue, 10, issue_params)
- expect { visit_milestone }.not_to exceed_query_limit(control_count)
+ expect { visit_milestone }.not_to exceed_query_limit(control)
end
end
diff --git a/spec/features/profiles/user_visits_profile_account_page_spec.rb b/spec/features/profiles/user_visits_profile_account_page_spec.rb
index 8c7233c77ad..a8c08a680d7 100644
--- a/spec/features/profiles/user_visits_profile_account_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_account_page_spec.rb
@@ -10,7 +10,6 @@ describe 'User visits the profile account page' do
end
it 'shows correct menu item' do
- expect(find('.sidebar-top-level-items > li.active')).to have_content('Account')
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
+ expect(page).to have_active_navigation('Account')
end
end
diff --git a/spec/features/profiles/user_visits_profile_authentication_log_page_spec.rb b/spec/features/profiles/user_visits_profile_authentication_log_page_spec.rb
deleted file mode 100644
index ffb504cc573..00000000000
--- a/spec/features/profiles/user_visits_profile_authentication_log_page_spec.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'spec_helper'
-
-describe 'User visits the authentication log page' do
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
-
- visit(audit_log_profile_path)
- end
-
- it 'shows correct menu item' do
- expect(find('.sidebar-top-level-items > li.active')).to have_content('Authentication log')
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
- end
-end
diff --git a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
new file mode 100644
index 00000000000..a50ebb29e01
--- /dev/null
+++ b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User visits the authentication log' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ visit(audit_log_profile_path)
+ end
+
+ it 'shows correct menu item' do
+ expect(page).to have_active_navigation('Authentication log')
+ end
+end
diff --git a/spec/features/profiles/user_visits_profile_page_spec.rb b/spec/features/profiles/user_visits_profile_page_spec.rb
deleted file mode 100644
index 3bf6d718bc7..00000000000
--- a/spec/features/profiles/user_visits_profile_page_spec.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'spec_helper'
-
-describe 'User visits the profile page' do
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
-
- visit(profile_path)
- end
-
- it 'shows correct menu item' do
- expect(find('.sidebar-top-level-items > li.active')).to have_content('Profile')
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
- end
-end
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
index fbc18d30e32..90d6841af0e 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -10,8 +10,7 @@ describe 'User visits the profile preferences page' do
end
it 'shows correct menu item' do
- expect(find('.sidebar-top-level-items > li.active')).to have_content('Preferences')
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
+ expect(page).to have_active_navigation('Preferences')
end
describe 'User changes their syntax highlighting theme', :js do
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
new file mode 100644
index 00000000000..6601d3039ed
--- /dev/null
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User visits their profile' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ visit(profile_path)
+ end
+
+ it 'shows correct menu item' do
+ expect(page).to have_active_navigation('Profile')
+ end
+end
diff --git a/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb
index 0b7a63b54b4..685bf44619d 100644
--- a/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb
@@ -10,7 +10,6 @@ describe 'User visits the profile SSH keys page' do
end
it 'shows correct menu item' do
- expect(find('.sidebar-top-level-items > li.active')).to have_content('SSH Keys')
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
+ expect(page).to have_active_navigation('SSH Keys')
end
end
diff --git a/spec/features/projects/commit/user_reverts_commit_spec.rb b/spec/features/projects/commit/user_reverts_commit_spec.rb
new file mode 100644
index 00000000000..221f1d7757e
--- /dev/null
+++ b/spec/features/projects/commit/user_reverts_commit_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe 'User reverts a commit', :js do
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ visit(project_commit_path(project, sample_commit.id))
+
+ find('.header-action-buttons .dropdown').click
+ find('a[href="#modal-revert-commit"]').click
+ end
+
+ context 'without creating a new merge request' do
+ before do
+ page.within('#modal-revert-commit') do
+ uncheck('create_merge_request')
+ click_button('Revert')
+ end
+ end
+
+ it 'reverts a commit' do
+ expect(page).to have_content('The commit has been successfully reverted.')
+ end
+
+ it 'does not revert a previously reverted commit' do
+ # Visit the comment again once it was reverted.
+ visit project_commit_path(project, sample_commit.id)
+
+ find('.header-action-buttons .dropdown').click
+ find('a[href="#modal-revert-commit"]').click
+
+ page.within('#modal-revert-commit') do
+ uncheck('create_merge_request')
+ click_button('Revert')
+ end
+
+ expect(page).to have_content('Sorry, we cannot revert this commit automatically.')
+ end
+ end
+
+ context 'with creating a new merge request' do
+ it 'reverts a commit' do
+ page.within('#modal-revert-commit') do
+ click_button('Revert')
+ end
+
+ expect(page).to have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
+ expect(page).to have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master")
+ end
+ end
+end
diff --git a/spec/features/projects/diffs/diff_show_spec.rb b/spec/features/projects/diffs/diff_show_spec.rb
index bc102895aaf..a6f52c9ef58 100644
--- a/spec/features/projects/diffs/diff_show_spec.rb
+++ b/spec/features/projects/diffs/diff_show_spec.rb
@@ -108,6 +108,19 @@ feature 'Diff file viewer', :js do
end
end
+ context 'renamed file' do
+ before do
+ visit_commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f')
+ end
+
+ it 'shows the filename with diff highlight' do
+ within('.file-header-content') do
+ expect(page).to have_css('.idiff.left.right.deletion')
+ expect(page).to have_content('files/js/commit.coffee')
+ end
+ end
+ end
+
context 'binary file that appears to be text in the first 1024 bytes' do
before do
# The file we're visiting is smaller than 10 KB and we want it collapsed
diff --git a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
new file mode 100644
index 00000000000..1bd2098af6d
--- /dev/null
+++ b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe 'User comments on a snippet', :js do
+ let(:project) { create(:project) }
+ let!(:snippet) { create(:project_snippet, project: project, author: user) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_snippet_path(project, snippet))
+ end
+
+ it 'leaves a comment on a snippet' do
+ page.within('.js-main-target-form') do
+ fill_in('note_note', with: 'Good snippet!')
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ expect(page).to have_content('Good snippet!')
+ end
+end
diff --git a/spec/features/projects/snippets/user_deletes_snippet_spec.rb b/spec/features/projects/snippets/user_deletes_snippet_spec.rb
new file mode 100644
index 00000000000..ca5f7981c33
--- /dev/null
+++ b/spec/features/projects/snippets/user_deletes_snippet_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe 'User deletes a snippet' do
+ let(:project) { create(:project) }
+ let!(:snippet) { create(:project_snippet, project: project, author: user) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_snippet_path(project, snippet))
+ end
+
+ it 'deletes a snippet' do
+ first(:link, 'Delete').click
+
+ expect(page).not_to have_content(snippet.title)
+ end
+end
diff --git a/spec/features/projects/snippets/user_updates_snippet_spec.rb b/spec/features/projects/snippets/user_updates_snippet_spec.rb
new file mode 100644
index 00000000000..09a390443cf
--- /dev/null
+++ b/spec/features/projects/snippets/user_updates_snippet_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe 'User updates a snippet' do
+ let(:project) { create(:project) }
+ let!(:snippet) { create(:project_snippet, project: project, author: user) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_snippet_path(project, snippet))
+ end
+
+ it 'updates a snippet' do
+ page.within('.detail-page-header') do
+ first(:link, 'Edit').click
+ end
+
+ fill_in('project_snippet_title', with: 'Snippet new title')
+ click_button('Save')
+
+ expect(page).to have_content('Snippet new title')
+ end
+end
diff --git a/spec/features/projects/snippets/user_views_snippets_spec.rb b/spec/features/projects/snippets/user_views_snippets_spec.rb
new file mode 100644
index 00000000000..e9992e00ca8
--- /dev/null
+++ b/spec/features/projects/snippets/user_views_snippets_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe 'User views snippets' do
+ let(:project) { create(:project) }
+ let!(:project_snippet) { create(:project_snippet, project: project, author: user) }
+ let!(:snippet) { create(:snippet, author: user) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_snippets_path(project))
+ end
+
+ it 'shows snippets' do
+ expect(page).to have_content(project_snippet.title)
+ expect(page).not_to have_content(snippet.title)
+ end
+end
diff --git a/spec/features/projects/user_archives_project_spec.rb b/spec/features/projects/user_archives_project_spec.rb
new file mode 100644
index 00000000000..72063d13c2a
--- /dev/null
+++ b/spec/features/projects/user_archives_project_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe 'User archives a project' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+
+ sign_in(user)
+ end
+
+ context 'when a project is archived' do
+ let(:project) { create(:project, :archived, namespace: user.namespace) }
+
+ before do
+ visit(edit_project_path(project))
+ end
+
+ it 'unarchives a project' do
+ expect(page).to have_content('Unarchive project')
+
+ click_link('Unarchive')
+
+ expect(page).not_to have_content('Archived project')
+ end
+ end
+
+ context 'when a project is unarchived' do
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+
+ before do
+ visit(edit_project_path(project))
+ end
+
+ it 'archives a project' do
+ expect(page).to have_content('Archive project')
+
+ click_link('Archive')
+
+ expect(page).to have_content('Archived')
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
new file mode 100644
index 00000000000..0ed797a62ea
--- /dev/null
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe 'User searches for code' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+
+ context 'when signed in' do
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it 'finds a file' do
+ visit(project_path(project))
+
+ page.within('.search') do
+ fill_in('search', with: 'application.js')
+ click_button('Go')
+ end
+
+ click_link('Code')
+
+ expect(page).to have_selector('.file-content .code')
+ expect(page).to have_selector("span.line[lang='javascript']")
+ end
+
+ context 'when on a project page', :js do
+ before do
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds code' do
+ find('.js-search-project-dropdown').trigger('click')
+
+ page.within('.project-filter') do
+ click_link(project.name_with_namespace)
+ end
+
+ fill_in('dashboard_search', with: 'rspec')
+ find('.btn-search').trigger('click')
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions')
+ end
+ end
+ end
+ end
+
+ context 'when signed out' do
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ visit(project_path(project))
+ end
+
+ it 'finds code' do
+ fill_in('search', with: 'rspec')
+ click_button('Go')
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions')
+ end
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_comments_spec.rb b/spec/features/search/user_searches_for_comments_spec.rb
new file mode 100644
index 00000000000..c7c469a262c
--- /dev/null
+++ b/spec/features/search/user_searches_for_comments_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe 'User searches for comments' do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ sign_in(user)
+
+ visit(project_path(project))
+ end
+
+ context 'when a comment is in commits' do
+ context 'when comment belongs to an invalid commit' do
+ let(:comment) { create(:note_on_commit, author: user, project: project, commit_id: 12345678, note: 'Bug here') }
+
+ it 'finds a commit' do
+ page.within('.search') do
+ fill_in('search', with: comment.note)
+ click_button('Go')
+ end
+
+ click_link('Comments')
+
+ expect(page).to have_text('Commit deleted')
+ expect(page).to have_text('12345678')
+ end
+ end
+ end
+
+ context 'when a comment is in a snippet' do
+ let(:snippet) { create(:project_snippet, :private, project: project, author: user, title: 'Some title') }
+ let(:comment) { create(:note, noteable: snippet, author: user, note: 'Supercalifragilisticexpialidocious', project: project) }
+
+ it 'finds a snippet' do
+ page.within('.search') do
+ fill_in('search', with: comment.note)
+ click_button('Go')
+ end
+
+ click_link('Comments')
+
+ expect(page).to have_link(snippet.title)
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_commits_spec.rb b/spec/features/search/user_searches_for_commits_spec.rb
new file mode 100644
index 00000000000..28cae444588
--- /dev/null
+++ b/spec/features/search/user_searches_for_commits_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe 'User searches for commits' do
+ let(:project) { create(:project, :repository) }
+ let(:sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ sign_in(user)
+
+ visit(search_path(project_id: project.id))
+ end
+
+ context 'when searching by SHA' do
+ it 'finds a commit and redirects to its page' do
+ fill_in('search', with: sha)
+ click_button('Search')
+
+ expect(page).to have_current_path(project_commit_path(project, sha))
+ end
+
+ it 'finds a commit in uppercase and redirects to its page' do
+ fill_in('search', with: sha.upcase)
+ click_button('Search')
+
+ expect(page).to have_current_path(project_commit_path(project, sha))
+ end
+ end
+
+ context 'when searching by message' do
+ it 'finds a commit and holds on /search page' do
+ create_commit('Message referencing another sha: "deadbeef"', project, user, 'master')
+
+ fill_in('search', with: 'deadbeef')
+ click_button('Search')
+
+ expect(page).to have_current_path('/search', only_path: true)
+ end
+
+ it 'finds multiple commits' do
+ fill_in('search', with: 'See merge request')
+ click_button('Search')
+ click_link('Commits')
+
+ expect(page).to have_selector('.commit-row-description', count: 9)
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
new file mode 100644
index 00000000000..630a81b1c5e
--- /dev/null
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -0,0 +1,76 @@
+require 'spec_helper'
+
+describe 'User searches for issues', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let!(:issue1) { create(:issue, title: 'Foo', project: project) }
+ let!(:issue2) { create(:issue, title: 'Bar', project: project) }
+
+ context 'when signed in' do
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds an issue' do
+ fill_in('dashboard_search', with: issue1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Issues')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title)
+ end
+ end
+
+ context 'when on a project page' do
+ it 'finds an issue' do
+ find('.js-search-project-dropdown').trigger('click')
+
+ page.within('.project-filter') do
+ click_link(project.name_with_namespace)
+ end
+
+ fill_in('dashboard_search', with: issue1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Issues')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title)
+ end
+ end
+ end
+ end
+
+ context 'when signed out' do
+ let(:project) { create(:project, :public) }
+
+ before do
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds an issue' do
+ fill_in('dashboard_search', with: issue1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Issues')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title)
+ end
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb
new file mode 100644
index 00000000000..116256682f4
--- /dev/null
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe 'User searches for merge requests', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let!(:merge_request1) { create(:merge_request, title: 'Foo', source_project: project, target_project: project) }
+ let!(:merge_request2) { create(:merge_request, :simple, title: 'Bar', source_project: project, target_project: project) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds a merge request' do
+ fill_in('dashboard_search', with: merge_request1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Merge requests')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(merge_request1.title).and have_no_link(merge_request2.title)
+ end
+ end
+
+ context 'when on a project page' do
+ it 'finds a merge request' do
+ find('.js-search-project-dropdown').trigger('click')
+
+ page.within('.project-filter') do
+ click_link(project.name_with_namespace)
+ end
+
+ fill_in('dashboard_search', with: merge_request1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Merge requests')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(merge_request1.title).and have_no_link(merge_request2.title)
+ end
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
new file mode 100644
index 00000000000..4fa9fe9ce8c
--- /dev/null
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe 'User searches for milestones', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let!(:milestone1) { create(:milestone, title: 'Foo', project: project) }
+ let!(:milestone2) { create(:milestone, title: 'Bar', project: project) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds a milestone' do
+ fill_in('dashboard_search', with: milestone1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Milestones')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(milestone1.title).and have_no_link(milestone2.title)
+ end
+ end
+
+ context 'when on a project page' do
+ it 'finds a milestone' do
+ find('.js-search-project-dropdown').trigger('click')
+
+ page.within('.project-filter') do
+ click_link(project.name_with_namespace)
+ end
+
+ fill_in('dashboard_search', with: milestone1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Milestones')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(milestone1.title).and have_no_link(milestone2.title)
+ end
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_projects_spec.rb b/spec/features/search/user_searches_for_projects_spec.rb
new file mode 100644
index 00000000000..242e437e41c
--- /dev/null
+++ b/spec/features/search/user_searches_for_projects_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe 'User searches for projects' do
+ let!(:project) { create(:project, :public, name: 'Shop') }
+
+ context 'when signed out' do
+ include_examples 'top right search form'
+
+ it 'finds a project' do
+ visit(search_path)
+
+ fill_in('dashboard_search', with: project.name[0..3])
+ click_button('Search')
+
+ expect(page).to have_link(project.name)
+ end
+
+ it 'preserves the group being searched in' do
+ visit(search_path(group_id: project.namespace.id))
+
+ fill_in('search', with: 'foo')
+ click_button('Search')
+
+ expect(find('#group_id', visible: false).value).to eq(project.namespace.id.to_s)
+ end
+
+ it 'preserves the project being searched in' do
+ visit(search_path(project_id: project.id))
+
+ fill_in('search', with: 'foo')
+ click_button('Search')
+
+ expect(find('#project_id', visible: false).value).to eq(project.id.to_s)
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
new file mode 100644
index 00000000000..1ea56479ecc
--- /dev/null
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe 'User searches for wiki pages', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'test_wiki', content: 'Some Wiki content' }) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds a page' do
+ find('.js-search-project-dropdown').trigger('click')
+
+ page.within('.project-filter') do
+ click_link(project.name_with_namespace)
+ end
+
+ fill_in('dashboard_search', with: 'content')
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Wiki')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(wiki_page.title)
+ end
+ end
+end
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
new file mode 100644
index 00000000000..5ddea36add5
--- /dev/null
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe 'User uses header search field' do
+ include FilteredSearchHelpers
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ sign_in(user)
+
+ visit(project_path(project))
+ end
+
+ it 'starts searching by pressing the enter key', :js do
+ fill_in('search', with: 'gitlab')
+ find('#search').native.send_keys(:enter)
+
+ page.within('.breadcrumbs-sub-title') do
+ expect(page).to have_content('Search')
+ end
+ end
+
+ it 'contains location badge' do
+ expect(page).to have_selector('.has-location-badge')
+ end
+
+ context 'when clicking the search field', :js do
+ before do
+ page.find('#search').click
+ end
+
+ it 'shows category search dropdown' do
+ expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i)
+ end
+
+ context 'when clicking issues' do
+ let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
+
+ it 'shows assigned issues' do
+ find('.dropdown-menu').click_link('Issues assigned to me')
+
+ expect(page).to have_selector('.filtered-search')
+ expect_tokens([assignee_token(user.name)])
+ expect_filtered_search_input_empty
+ end
+
+ it 'shows created issues' do
+ find('.dropdown-menu').click_link("Issues I've created")
+
+ expect(page).to have_selector('.filtered-search')
+ expect_tokens([author_token(user.name)])
+ expect_filtered_search_input_empty
+ end
+ end
+
+ context 'when clicking merge requests' do
+ let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
+
+ it 'shows assigned merge requests' do
+ find('.dropdown-menu').click_link('Merge requests assigned to me')
+
+ expect(page).to have_selector('.merge-requests-holder')
+ expect_tokens([assignee_token(user.name)])
+ expect_filtered_search_input_empty
+ end
+
+ it 'shows created merge requests' do
+ find('.dropdown-menu').click_link("Merge requests I've created")
+
+ expect(page).to have_selector('.merge-requests-holder')
+ expect_tokens([author_token(user.name)])
+ expect_filtered_search_input_empty
+ end
+ end
+ end
+
+ context 'when entering text into the search field', :js do
+ before do
+ page.within('.search-input-wrap') do
+ fill_in('search', with: project.name[0..3])
+ end
+ end
+
+ it 'does not display the category search dropdown' do
+ expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i)
+ end
+ end
+end
diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb
new file mode 100644
index 00000000000..95f3eb5e805
--- /dev/null
+++ b/spec/features/search/user_uses_search_filters_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe 'User uses search filters', :js do
+ let(:group) { create(:group) }
+ let!(:group_project) { create(:project, group: group) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ group.add_owner(user)
+ sign_in(user)
+
+ visit(search_path)
+ end
+
+ context' when filtering by group' do
+ it 'shows group projects' do
+ find('.js-search-group-dropdown').trigger('click')
+
+ wait_for_requests
+
+ page.within('.search-holder') do
+ click_link(group.name)
+ end
+
+ expect(find('.js-search-group-dropdown')).to have_content(group.name)
+
+ page.within('.project-filter') do
+ find('.js-search-project-dropdown').trigger('click')
+
+ wait_for_requests
+
+ expect(page).to have_link(group_project.name_with_namespace)
+ end
+ end
+ end
+
+ context' when filtering by project' do
+ it 'shows a project' do
+ page.within('.project-filter') do
+ find('.js-search-project-dropdown').trigger('click')
+
+ wait_for_requests
+
+ click_link(project.name_with_namespace)
+ end
+
+ expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace)
+ end
+ end
+end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
deleted file mode 100644
index b9b3d10b5ef..00000000000
--- a/spec/features/search_spec.rb
+++ /dev/null
@@ -1,310 +0,0 @@
-require 'spec_helper'
-
-describe "Search" do
- include FilteredSearchHelpers
-
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
- let!(:issue) { create(:issue, project: project, assignees: [user]) }
- let!(:issue2) { create(:issue, project: project, author: user) }
-
- before do
- sign_in(user)
- project.team << [user, :reporter]
- visit search_path
- end
-
- it 'does not show top right search form' do
- expect(page).not_to have_selector('.search')
- end
-
- context 'search filters', js: true do
- let(:group) { create(:group) }
- let!(:group_project) { create(:project, group: group) }
-
- before do
- group.add_owner(user)
- end
-
- it 'shows group name after filtering' do
- find('.js-search-group-dropdown').click
- wait_for_requests
-
- page.within '.search-holder' do
- click_link group.name
- end
-
- expect(find('.js-search-group-dropdown')).to have_content(group.name)
- end
-
- it 'filters by group projects after filtering by group' do
- find('.js-search-group-dropdown').click
- wait_for_requests
-
- page.within '.search-holder' do
- click_link group.name
- end
-
- expect(find('.js-search-group-dropdown')).to have_content(group.name)
-
- page.within('.project-filter') do
- find('.js-search-project-dropdown').click
- wait_for_requests
-
- expect(page).to have_link(group_project.name_with_namespace)
- end
- end
-
- it 'shows project name after filtering' do
- page.within('.project-filter') do
- find('.js-search-project-dropdown').click
- wait_for_requests
-
- click_link project.name_with_namespace
- end
-
- expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace)
- end
- end
-
- describe 'searching for Projects' do
- it 'finds a project' do
- page.within '.search-holder' do
- fill_in "search", with: project.name[0..3]
- click_button "Search"
- end
-
- expect(page).to have_content project.name
- end
- end
-
- context 'search for comments' do
- context 'when comment belongs to a invalid commit' do
- let(:project) { create(:project, :repository) }
- let(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'Bug here') }
-
- before do
- note.update_attributes(commit_id: 12345678)
- end
-
- it 'finds comment' do
- visit project_path(project)
-
- page.within '.search' do
- fill_in 'search', with: note.note
- click_button 'Go'
- end
-
- click_link 'Comments'
-
- expect(page).to have_text("Commit deleted")
- expect(page).to have_text("12345678")
- end
- end
-
- it 'finds a snippet' do
- snippet = create(:project_snippet, :private, project: project, author: user, title: 'Some title')
- note = create(:note,
- noteable: snippet,
- author: user,
- note: 'Supercalifragilisticexpialidocious',
- project: project)
- # Must visit project dashboard since global search won't search
- # everything (e.g. comments, snippets, etc.)
- visit project_path(project)
-
- page.within '.search' do
- fill_in 'search', with: note.note
- click_button 'Go'
- end
-
- click_link 'Comments'
-
- expect(page).to have_link(snippet.title)
- end
-
- it 'finds a commit' do
- project = create(:project, :repository) { |p| p.add_reporter(user) }
- visit project_path(project)
-
- page.within '.search' do
- fill_in 'search', with: 'add'
- click_button 'Go'
- end
-
- click_link "Commits"
-
- expect(page).to have_selector('.commit-row-description')
- end
-
- it 'finds a code' do
- project = create(:project, :repository) { |p| p.add_reporter(user) }
- visit project_path(project)
-
- page.within '.search' do
- fill_in 'search', with: 'application.js'
- click_button 'Go'
- end
-
- click_link "Code"
-
- expect(page).to have_selector('.file-content .code')
-
- expect(page).to have_selector("span.line[lang='javascript']")
- end
- end
-
- describe 'Right header search field' do
- it 'allows enter key to search', js: true do
- visit project_path(project)
- fill_in 'search', with: 'gitlab'
- find('#search').native.send_keys(:enter)
-
- page.within '.breadcrumbs-sub-title' do
- expect(page).to have_content 'Search'
- end
- end
-
- describe 'Search in project page' do
- before do
- visit project_path(project)
- end
-
- it 'shows top right search form' do
- expect(page).to have_selector('#search')
- end
-
- it 'contains location badge in top right search form' do
- expect(page).to have_selector('.has-location-badge')
- end
-
- context 'clicking the search field', js: true do
- it 'shows category search dropdown' do
- page.find('#search').click
-
- expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i)
- end
- end
-
- context 'click the links in the category search dropdown', js: true do
- let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
-
- before do
- page.find('#search').click
- end
-
- it 'takes user to her issues page when issues assigned is clicked' do
- find('.dropdown-menu').click_link 'Issues assigned to me'
-
- expect(page).to have_selector('.filtered-search')
- expect_tokens([assignee_token(user.name)])
- expect_filtered_search_input_empty
- end
-
- it 'takes user to her issues page when issues authored is clicked' do
- find('.dropdown-menu').click_link "Issues I've created"
-
- expect(page).to have_selector('.filtered-search')
- expect_tokens([author_token(user.name)])
- expect_filtered_search_input_empty
- end
-
- it 'takes user to her MR page when MR assigned is clicked' do
- find('.dropdown-menu').click_link 'Merge requests assigned to me'
-
- expect(page).to have_selector('.merge-requests-holder')
- expect_tokens([assignee_token(user.name)])
- expect_filtered_search_input_empty
- end
-
- it 'takes user to her MR page when MR authored is clicked' do
- find('.dropdown-menu').click_link "Merge requests I've created"
-
- expect(page).to have_selector('.merge-requests-holder')
- expect_tokens([author_token(user.name)])
- expect_filtered_search_input_empty
- end
- end
-
- context 'entering text into the search field', js: true do
- before do
- page.within '.search-input-wrap' do
- fill_in "search", with: project.name[0..3]
- end
- end
-
- it 'does not display the category search dropdown' do
- expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i)
- end
- end
- end
- end
-
- describe 'search for commits' do
- let(:project) { create(:project, :repository) }
-
- before do
- visit search_path(project_id: project.id)
- end
-
- it 'redirects to commit page when search by sha and only commit found' do
- fill_in 'search', with: '6d394385cf567f80a8fd85055db1ab4c5295806f'
-
- click_button 'Search'
-
- expect(page).to have_current_path(project_commit_path(project, '6d394385cf567f80a8fd85055db1ab4c5295806f'))
- end
-
- it 'redirects to single commit regardless of query case' do
- fill_in 'search', with: '6D394385cf'
-
- click_button 'Search'
-
- expect(page).to have_current_path(project_commit_path(project, '6d394385cf567f80a8fd85055db1ab4c5295806f'))
- end
-
- it 'holds on /search page when the only commit is found by message' do
- create_commit('Message referencing another sha: "deadbeef" ', project, user, 'master')
-
- fill_in 'search', with: 'deadbeef'
- click_button 'Search'
-
- expect(page).to have_current_path('/search', only_path: true)
- end
-
- it 'shows multiple matching commits' do
- fill_in 'search', with: 'See merge request'
-
- click_button 'Search'
- click_link 'Commits'
-
- expect(page).to have_selector('.commit-row-description', count: 9)
- end
- end
-
- context 'anonymous user' do
- let(:project) { create(:project, :public) }
-
- before do
- sign_out(user)
- end
-
- it 'preserves the group being searched in' do
- visit search_path(group_id: project.namespace.id)
-
- fill_in 'search', with: 'foo'
- click_button 'Search'
-
- expect(find('#group_id', visible: false).value).to eq(project.namespace.id.to_s)
- end
-
- it 'preserves the project being searched in' do
- visit search_path(project_id: project.id)
-
- fill_in 'search', with: 'foo'
- click_button 'Search'
-
- expect(find('#project_id', visible: false).value).to eq(project.id.to_s)
- end
- end
-end
diff --git a/spec/finders/autocomplete_users_finder_spec.rb b/spec/finders/autocomplete_users_finder_spec.rb
new file mode 100644
index 00000000000..684af74d750
--- /dev/null
+++ b/spec/finders/autocomplete_users_finder_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe AutocompleteUsersFinder do
+ describe '#execute' do
+ let!(:user1) { create(:user, username: 'johndoe') }
+ let!(:user2) { create(:user, :blocked, username: 'notsorandom') }
+ let!(:external_user) { create(:user, :external) }
+ let!(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
+ let(:current_user) { create(:user) }
+ let(:params) { {} }
+
+ let(:project) { nil }
+ let(:group) { nil }
+
+ subject { described_class.new(params: params, current_user: current_user, project: project, group: group).execute.to_a }
+
+ context 'when current_user not passed or nil' do
+ let(:current_user) { nil }
+
+ it { is_expected.to match_array([]) }
+ end
+
+ context 'when project passed' do
+ let(:project) { create(:project) }
+
+ it { is_expected.to match_array([project.owner]) }
+
+ context 'when author_id passed' do
+ let(:params) { { author_id: user2.id } }
+
+ it { is_expected.to match_array([project.owner, user2]) }
+ end
+ end
+
+ context 'when group passed and project not passed' do
+ let(:group) { create(:group, :public) }
+
+ before do
+ group.add_users([user1], GroupMember::DEVELOPER)
+ end
+
+ it { is_expected.to match_array([user1]) }
+ end
+
+ it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) }
+
+ context 'when filtered by search' do
+ let(:params) { { search: 'johndoe' } }
+
+ it { is_expected.to match_array([user1]) }
+ end
+
+ context 'when filtered by skip_users' do
+ let(:params) { { skip_users: [omniauth_user.id, current_user.id] } }
+
+ it { is_expected.to match_array([user1, external_user]) }
+ end
+
+ context 'when todos exist' do
+ let!(:pending_todo1) { create(:todo, user: current_user, author: user1, state: :pending) }
+ let!(:pending_todo2) { create(:todo, user: external_user, author: omniauth_user, state: :pending) }
+ let!(:done_todo1) { create(:todo, user: current_user, author: external_user, state: :done) }
+ let!(:done_todo2) { create(:todo, user: user1, author: external_user, state: :done) }
+
+ context 'when filtered by todo_filter without todo_state_filter' do
+ let(:params) { { todo_filter: true } }
+
+ it { is_expected.to match_array([]) }
+ end
+
+ context 'when filtered by todo_filter with pending todo_state_filter' do
+ let(:params) { { todo_filter: true, todo_state_filter: 'pending' } }
+
+ it { is_expected.to match_array([user1]) }
+ end
+
+ context 'when filtered by todo_filter with done todo_state_filter' do
+ let(:params) { { todo_filter: true, todo_state_filter: 'done' } }
+
+ it { is_expected.to match_array([external_user]) }
+ end
+ end
+
+ context 'when filtered by current_user' do
+ let(:current_user) { user2 }
+ let(:params) { { current_user: true } }
+
+ it { is_expected.to match_array([user2, user1, external_user, omniauth_user]) }
+ end
+
+ context 'when filtered by author_id' do
+ let(:params) { { author_id: user2.id } }
+
+ it { is_expected.to match_array([user2, user1, external_user, omniauth_user, current_user]) }
+ end
+ end
+end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 0deea0ff6a3..f9c31ac61d8 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -136,9 +136,9 @@ describe DiffHelper do
marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
expect(marked_old_line).to eq(%q{abc <span class="idiff left right deletion">'def'</span>})
- expect(marked_old_line).not_to be_html_safe
+ expect(marked_old_line).to be_html_safe
expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">"def"</span>})
- expect(marked_new_line).not_to be_html_safe
+ expect(marked_new_line).to be_html_safe
end
end
diff --git a/spec/initializers/doorkeeper_spec.rb b/spec/initializers/doorkeeper_spec.rb
index 37cc08b3038..1a78196e33d 100644
--- a/spec/initializers/doorkeeper_spec.rb
+++ b/spec/initializers/doorkeeper_spec.rb
@@ -9,8 +9,8 @@ describe Doorkeeper.configuration do
end
describe '#optional_scopes' do
- it 'matches Gitlab::Auth::OPTIONAL_SCOPES' do
- expect(subject.optional_scopes).to eq Gitlab::Auth::OPTIONAL_SCOPES - Gitlab::Auth::REGISTRY_SCOPES
+ it 'matches Gitlab::Auth.optional_scopes' do
+ expect(subject.optional_scopes).to eq Gitlab::Auth.optional_scopes - Gitlab::Auth::REGISTRY_SCOPES
end
end
diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js
index b3c9bca64cc..02415485d19 100644
--- a/spec/javascripts/filtered_search/dropdown_user_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_user_spec.js
@@ -10,6 +10,7 @@ describe('Dropdown User', () => {
beforeEach(() => {
spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
+ spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {});
spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {});
dropdownUser = new gl.DropdownUser({
@@ -38,6 +39,7 @@ describe('Dropdown User', () => {
beforeEach(() => {
spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
+ spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {});
});
it('should return endpoint', () => {
diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js
index f8b37c0edde..f4b4d7980a4 100644
--- a/spec/javascripts/fly_out_nav_spec.js
+++ b/spec/javascripts/fly_out_nav_spec.js
@@ -271,12 +271,19 @@ describe('Fly out sidebar navigation', () => {
});
it('sets transform of sub-items', () => {
+ const sidebar = document.createElement('div');
const subItems = el.querySelector('.sidebar-sub-level-items');
+
+ sidebar.style.width = '200px';
+
+ document.body.appendChild(sidebar);
+
+ setSidebar(sidebar);
showSubLevelItems(el);
expect(
subItems.style.transform,
- ).toBe(`translate3d(0px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`);
+ ).toBe(`translate3d(200px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`);
});
it('sets is-above when element is above', () => {
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 4f4a27e4c41..af1db2c3455 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -16,20 +16,20 @@ describe Gitlab::Auth do
expect(subject::DEFAULT_SCOPES).to eq [:api]
end
- it 'OPTIONAL_SCOPES contains all non-default scopes' do
+ it 'optional_scopes contains all non-default scopes' do
stub_container_registry_config(enabled: true)
- expect(subject::OPTIONAL_SCOPES).to eq %i[read_user read_registry openid]
+ expect(subject.optional_scopes).to eq %i[read_user read_registry openid]
end
- context 'REGISTRY_SCOPES' do
+ context 'registry_scopes' do
context 'when registry is disabled' do
before do
stub_container_registry_config(enabled: false)
end
it 'is empty' do
- expect(subject::REGISTRY_SCOPES).to eq []
+ expect(subject.registry_scopes).to eq []
end
end
@@ -39,7 +39,7 @@ describe Gitlab::Auth do
end
it 'contains all registry related scopes' do
- expect(subject::REGISTRY_SCOPES).to eq %i[read_registry]
+ expect(subject.registry_scopes).to eq %i[read_registry]
end
end
end
diff --git a/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb
new file mode 100644
index 00000000000..5c471cbdeda
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::DeleteConflictingRedirectRoutesRange, :migration, schema: 20170907170235 do
+ let!(:redirect_routes) { table(:redirect_routes) }
+ let!(:routes) { table(:routes) }
+
+ before do
+ routes.create!(id: 1, source_id: 1, source_type: 'Namespace', path: 'foo1')
+ routes.create!(id: 2, source_id: 2, source_type: 'Namespace', path: 'foo2')
+ routes.create!(id: 3, source_id: 3, source_type: 'Namespace', path: 'foo3')
+ routes.create!(id: 4, source_id: 4, source_type: 'Namespace', path: 'foo4')
+ routes.create!(id: 5, source_id: 5, source_type: 'Namespace', path: 'foo5')
+
+ # Valid redirects
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar2')
+ redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'bar3')
+
+ # Conflicting redirects
+ redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'foo1')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo2')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo3')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo4')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5')
+ end
+
+ it 'deletes the conflicting redirect_routes in the range' do
+ expect(redirect_routes.count).to eq(8)
+
+ expect do
+ described_class.new.perform(1, 3)
+ end.to change { redirect_routes.where("path like 'foo%'").count }.from(5).to(2)
+
+ expect do
+ described_class.new.perform(4, 5)
+ end.to change { redirect_routes.where("path like 'foo%'").count }.from(2).to(0)
+
+ expect(redirect_routes.count).to eq(3)
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 1bcdc369c44..3c8350b3aad 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -914,4 +914,126 @@ describe Gitlab::Database::MigrationHelpers do
.to raise_error(RuntimeError, /Your database user is not allowed/)
end
end
+
+ describe '#bulk_queue_background_migration_jobs_by_range', :sidekiq do
+ context 'when the model has an ID column' do
+ let!(:id1) { create(:user).id }
+ let!(:id2) { create(:user).id }
+ let!(:id3) { create(:user).id }
+
+ before do
+ User.class_eval do
+ include EachBatch
+ end
+ end
+
+ context 'with enough rows to bulk queue jobs more than once' do
+ before do
+ stub_const('Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE', 1)
+ end
+
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2)
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
+ end
+ end
+
+ it 'queues jobs in groups of buffer size 1' do
+ expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id1, id2]]])
+ expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id3, id3]]])
+
+ model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2)
+ end
+ end
+
+ context 'with not enough rows to bulk queue jobs more than once' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2)
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
+ end
+ end
+
+ it 'queues jobs in bulk all at once (big buffer size)' do
+ expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id1, id2]],
+ ['FooJob', [id3, id3]]])
+
+ model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2)
+ end
+ end
+
+ context 'without specifying batch_size' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob')
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]])
+ end
+ end
+ end
+ end
+
+ context "when the model doesn't have an ID column" do
+ it 'raises error (for now)' do
+ expect do
+ model.bulk_queue_background_migration_jobs_by_range(ProjectAuthorization, 'FooJob')
+ end.to raise_error(StandardError, /does not have an ID/)
+ end
+ end
+ end
+
+ describe '#queue_background_migration_jobs_by_range_at_intervals', :sidekiq do
+ context 'when the model has an ID column' do
+ let!(:id1) { create(:user).id }
+ let!(:id2) { create(:user).id }
+ let!(:id3) { create(:user).id }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ before do
+ User.class_eval do
+ include EachBatch
+ end
+ end
+
+ context 'with batch_size option' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds, batch_size: 2)
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
+ expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.seconds.from_now.to_f)
+ end
+ end
+ end
+
+ context 'without batch_size option' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds)
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f)
+ end
+ end
+ end
+ end
+
+ context "when the model doesn't have an ID column" do
+ it 'raises error (for now)' do
+ expect do
+ model.queue_background_migration_jobs_by_range_at_intervals(ProjectAuthorization, 'FooJob', 10.seconds)
+ end.to raise_error(StandardError, /does not have an ID/)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index 8beebc10040..4fa30d8df8b 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -1761,17 +1761,9 @@ describe Gitlab::Diff::PositionTracer do
let(:merge_commit) do
update_file_again_commit
- committer = repository.user_to_committer(current_user)
-
- options = {
- message: "Merge branches",
- author: committer,
- committer: committer
- }
-
merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project)
- repository.merge(current_user, merge_request.diff_head_sha, merge_request, options)
+ repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches")
project.commit(branch_name)
end
diff --git a/spec/lib/gitlab/git/hooks_service_spec.rb b/spec/lib/gitlab/git/hooks_service_spec.rb
index e9c0209fe3b..d4d75b66659 100644
--- a/spec/lib/gitlab/git/hooks_service_spec.rb
+++ b/spec/lib/gitlab/git/hooks_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Git::HooksService, seed_helper: true do
- let(:committer) { Gitlab::Git::Committer.new('Jane Doe', 'janedoe@example.com', 'user-456') }
+ let(:user) { Gitlab::Git::User.new('Jane Doe', 'janedoe@example.com', 'user-456') }
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, 'project-123') }
let(:service) { described_class.new }
@@ -18,7 +18,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do
hook = double(trigger: [true, nil])
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
- service.execute(committer, repository, @blankrev, @newrev, @ref) { }
+ service.execute(user, repository, @blankrev, @newrev, @ref) { }
end
end
@@ -28,7 +28,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
- service.execute(committer, repository, @blankrev, @newrev, @ref)
+ service.execute(user, repository, @blankrev, @newrev, @ref)
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end
end
@@ -40,7 +40,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
- service.execute(committer, repository, @blankrev, @newrev, @ref)
+ service.execute(user, repository, @blankrev, @newrev, @ref)
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end
end
diff --git a/spec/lib/gitlab/git/committer_spec.rb b/spec/lib/gitlab/git/user_spec.rb
index b0ddbb51449..0ebcecb26c0 100644
--- a/spec/lib/gitlab/git/committer_spec.rb
+++ b/spec/lib/gitlab/git/user_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Git::Committer do
+describe Gitlab::Git::User do
let(:name) { 'Jane Doe' }
let(:email) { 'janedoe@example.com' }
let(:gl_id) { 'user-123' }
diff --git a/spec/lib/gitlab/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb
index 08010c2d0e2..8dc83a6db7f 100644
--- a/spec/lib/gitlab/group_hierarchy_spec.rb
+++ b/spec/lib/gitlab/group_hierarchy_spec.rb
@@ -23,6 +23,11 @@ describe Gitlab::GroupHierarchy, :postgresql do
expect(relation).to include(parent, child1, child2)
end
+
+ it 'does not allow the use of #update_all' do
+ expect { relation.update_all(share_with_group_lock: false) }
+ .to raise_error(ActiveRecord::ReadOnlyRecord)
+ end
end
describe '#base_and_descendants' do
@@ -43,6 +48,11 @@ describe Gitlab::GroupHierarchy, :postgresql do
expect(relation).to include(parent, child1, child2)
end
+
+ it 'does not allow the use of #update_all' do
+ expect { relation.update_all(share_with_group_lock: false) }
+ .to raise_error(ActiveRecord::ReadOnlyRecord)
+ end
end
describe '#all_groups' do
@@ -73,5 +83,10 @@ describe Gitlab::GroupHierarchy, :postgresql do
expect(relation).to include(child2)
end
+
+ it 'does not allow the use of #update_all' do
+ expect { relation.update_all(share_with_group_lock: false) }
+ .to raise_error(ActiveRecord::ReadOnlyRecord)
+ end
end
end
diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb
index d5f4da3ce36..dbcc200b90b 100644
--- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb
+++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb
@@ -1,10 +1,11 @@
require 'spec_helper'
describe Gitlab::OAuth::AuthHash do
+ let(:provider) { 'ldap'.freeze }
let(:auth_hash) do
described_class.new(
OmniAuth::AuthHash.new(
- provider: provider_ascii,
+ provider: provider,
uid: uid_ascii,
info: info_hash
)
@@ -20,7 +21,6 @@ describe Gitlab::OAuth::AuthHash do
let(:last_name_raw) { "K\xC3\xBC\xC3\xA7\xC3\xBCk" }
let(:name_raw) { "Onur K\xC3\xBC\xC3\xA7\xC3\xBCk" }
- let(:provider_ascii) { 'ldap'.force_encoding(Encoding::ASCII_8BIT) }
let(:uid_ascii) { uid_raw.force_encoding(Encoding::ASCII_8BIT) }
let(:email_ascii) { email_raw.force_encoding(Encoding::ASCII_8BIT) }
let(:nickname_ascii) { nickname_raw.force_encoding(Encoding::ASCII_8BIT) }
@@ -28,7 +28,6 @@ describe Gitlab::OAuth::AuthHash do
let(:last_name_ascii) { last_name_raw.force_encoding(Encoding::ASCII_8BIT) }
let(:name_ascii) { name_raw.force_encoding(Encoding::ASCII_8BIT) }
- let(:provider_utf8) { provider_ascii.force_encoding(Encoding::UTF_8) }
let(:uid_utf8) { uid_ascii.force_encoding(Encoding::UTF_8) }
let(:email_utf8) { email_ascii.force_encoding(Encoding::UTF_8) }
let(:nickname_utf8) { nickname_ascii.force_encoding(Encoding::UTF_8) }
@@ -46,7 +45,7 @@ describe Gitlab::OAuth::AuthHash do
end
context 'defaults' do
- it { expect(auth_hash.provider).to eql provider_utf8 }
+ it { expect(auth_hash.provider).to eq provider }
it { expect(auth_hash.uid).to eql uid_utf8 }
it { expect(auth_hash.email).to eql email_utf8 }
it { expect(auth_hash.username).to eql nickname_utf8 }
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index fdc3990132a..59c28431e1e 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -174,4 +174,13 @@ describe Gitlab::UrlSanitizer do
end
end
end
+
+ context 'when credentials contains special chars' do
+ it 'should parse the URL without errors' do
+ url_sanitizer = described_class.new("https://foo:b?r@github.com/me/project.git")
+
+ expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git")
+ expect(url_sanitizer.full_url).to eq("https://foo:b?r@github.com/me/project.git")
+ end
+ end
end
diff --git a/spec/migrations/delete_conflicting_redirect_routes_spec.rb b/spec/migrations/delete_conflicting_redirect_routes_spec.rb
new file mode 100644
index 00000000000..1df2477da51
--- /dev/null
+++ b/spec/migrations/delete_conflicting_redirect_routes_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170907170235_delete_conflicting_redirect_routes')
+
+describe DeleteConflictingRedirectRoutes, :migration, :sidekiq do
+ let!(:redirect_routes) { table(:redirect_routes) }
+ let!(:routes) { table(:routes) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ before do
+ stub_const("DeleteConflictingRedirectRoutes::BATCH_SIZE", 2)
+ stub_const("Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE", 2)
+
+ routes.create!(id: 1, source_id: 1, source_type: 'Namespace', path: 'foo1')
+ routes.create!(id: 2, source_id: 2, source_type: 'Namespace', path: 'foo2')
+ routes.create!(id: 3, source_id: 3, source_type: 'Namespace', path: 'foo3')
+ routes.create!(id: 4, source_id: 4, source_type: 'Namespace', path: 'foo4')
+ routes.create!(id: 5, source_id: 5, source_type: 'Namespace', path: 'foo5')
+
+ # Valid redirects
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar2')
+ redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'bar3')
+
+ # Conflicting redirects
+ redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'foo1')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo2')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo3')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo4')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5')
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([described_class::MIGRATION, [1, 2]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(12.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([described_class::MIGRATION, [3, 4]])
+ expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(24.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[2]['args']).to eq([described_class::MIGRATION, [5, 5]])
+ expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(36.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 3
+ end
+ end
+ end
+
+ it 'schedules background migrations' do
+ Sidekiq::Testing.inline! do
+ expect do
+ migrate!
+ end.to change { redirect_routes.count }.from(8).to(3)
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 95da97b7bc5..77f0be6b120 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1439,4 +1439,24 @@ describe Ci::Pipeline, :mailer do
it_behaves_like 'not sending any notification'
end
end
+
+ describe '#latest_builds_with_artifacts' do
+ let!(:pipeline) { create(:ci_pipeline, :success) }
+
+ let!(:build) do
+ create(:ci_build, :success, :artifacts, pipeline: pipeline)
+ end
+
+ it 'returns the latest builds' do
+ expect(pipeline.latest_builds_with_artifacts).to eq([build])
+ end
+
+ it 'memoizes the returned relation' do
+ query_count = ActiveRecord::QueryRecorder
+ .new { 2.times { pipeline.latest_builds_with_artifacts.to_a } }
+ .count
+
+ expect(query_count).to eq(1)
+ end
+ end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 11e64a0f877..e3cfa149e3a 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -207,11 +207,6 @@ eos
context 'of a merge commit' do
let(:repository) { project.repository }
- let(:commit_options) do
- author = repository.user_to_committer(user)
- { message: 'Test message', committer: author, author: author }
- end
-
let(:merge_request) do
create(:merge_request,
source_branch: 'video',
@@ -224,7 +219,7 @@ eos
merge_commit_id = repository.merge(user,
merge_request.diff_head_sha,
merge_request,
- commit_options)
+ 'Test message')
repository.commit(merge_commit_id)
end
diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb
index 9c99c3e5c08..fadc8bfeb61 100644
--- a/spec/models/gpg_key_spec.rb
+++ b/spec/models/gpg_key_spec.rb
@@ -140,18 +140,6 @@ describe GpgKey do
end
end
- describe 'notification', :mailer do
- let(:user) { create(:user) }
-
- it 'sends a notification' do
- perform_enqueued_jobs do
- create(:gpg_key, user: user)
- end
-
- should_email(user)
- end
- end
-
describe '#revoke' do
it 'invalidates all associated gpg signatures and destroys the key' do
gpg_key = create :gpg_key
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 96baeaff0a4..dbc4aba8547 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -169,16 +169,4 @@ describe Key, :mailer do
expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key)
end
end
-
- describe 'notification' do
- let(:user) { create(:user) }
-
- it 'sends a notification' do
- perform_enqueued_jobs do
- create(:key, user: user)
- end
-
- should_email(user)
- end
- end
end
diff --git a/spec/models/project_services/pipelines_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb
index 5faab9ba38b..be07ca2d945 100644
--- a/spec/models/project_services/pipelines_email_service_spec.rb
+++ b/spec/models/project_services/pipelines_email_service_spec.rb
@@ -6,7 +6,8 @@ describe PipelinesEmailService, :mailer do
end
let(:project) { create(:project, :repository) }
- let(:recipient) { 'test@gitlab.com' }
+ let(:recipients) { 'test@gitlab.com' }
+ let(:receivers) { [recipients] }
let(:data) do
Gitlab::DataBuilder::Pipeline.build(pipeline)
@@ -48,18 +49,24 @@ describe PipelinesEmailService, :mailer do
shared_examples 'sending email' do
before do
+ subject.recipients = recipients
+
perform_enqueued_jobs do
run
end
end
it 'sends email' do
- should_only_email(double(notification_email: recipient), kind: :bcc)
+ emails = receivers.map { |r| double(notification_email: r) }
+
+ should_only_email(*emails, kind: :bcc)
end
end
shared_examples 'not sending email' do
before do
+ subject.recipients = recipients
+
perform_enqueued_jobs do
run
end
@@ -75,10 +82,6 @@ describe PipelinesEmailService, :mailer do
subject.test(data)
end
- before do
- subject.recipients = recipient
- end
-
context 'when pipeline is failed' do
before do
data[:object_attributes][:status] = 'failed'
@@ -104,10 +107,6 @@ describe PipelinesEmailService, :mailer do
end
context 'with recipients' do
- before do
- subject.recipients = recipient
- end
-
context 'with failed pipeline' do
before do
data[:object_attributes][:status] = 'failed'
@@ -152,9 +151,7 @@ describe PipelinesEmailService, :mailer do
end
context 'with empty recipients list' do
- before do
- subject.recipients = ' ,, '
- end
+ let(:recipients) { ' ,, ' }
context 'with failed pipeline' do
before do
@@ -165,5 +162,19 @@ describe PipelinesEmailService, :mailer do
it_behaves_like 'not sending email'
end
end
+
+ context 'with recipients list separating with newlines' do
+ let(:recipients) { "\ntest@gitlab.com, \r\nexample@gitlab.com" }
+ let(:receivers) { %w[test@gitlab.com example@gitlab.com] }
+
+ context 'with failed pipeline' do
+ before do
+ data[:object_attributes][:status] = 'failed'
+ pipeline.update(status: 'failed')
+ end
+
+ it_behaves_like 'sending email'
+ end
+ end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 48fc77423ff..78226c6c3fa 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -2682,4 +2682,60 @@ describe Project do
end
end
end
+
+ describe '#latest_successful_builds_for' do
+ let(:project) { build(:project) }
+
+ before do
+ allow(project).to receive(:default_branch).and_return('master')
+ end
+
+ context 'without a ref' do
+ it 'returns a pipeline for the default branch' do
+ expect(project)
+ .to receive(:latest_successful_pipeline_for_default_branch)
+
+ project.latest_successful_pipeline_for
+ end
+ end
+
+ context 'with the ref set to the default branch' do
+ it 'returns a pipeline for the default branch' do
+ expect(project)
+ .to receive(:latest_successful_pipeline_for_default_branch)
+
+ project.latest_successful_pipeline_for(project.default_branch)
+ end
+ end
+
+ context 'with a ref that is not the default branch' do
+ it 'returns the latest successful pipeline for the given ref' do
+ expect(project.pipelines).to receive(:latest_successful_for).with('foo')
+
+ project.latest_successful_pipeline_for('foo')
+ end
+ end
+ end
+
+ describe '#latest_successful_pipeline_for_default_branch' do
+ let(:project) { build(:project) }
+
+ before do
+ allow(project).to receive(:default_branch).and_return('master')
+ end
+
+ it 'memoizes and returns the latest successful pipeline for the default branch' do
+ pipeline = double(:pipeline)
+
+ expect(project.pipelines).to receive(:latest_successful_for)
+ .with(project.default_branch)
+ .and_return(pipeline)
+ .once
+
+ 2.times do
+ expect(project.latest_successful_pipeline_for_default_branch)
+ .to eq(pipeline)
+ end
+ end
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 7065d467ec0..60cd7e70055 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -8,12 +8,9 @@ describe Repository, models: true do
let(:repository) { project.repository }
let(:broken_repository) { create(:project, :broken_storage).repository }
let(:user) { create(:user) }
- let(:committer) { Gitlab::Git::Committer.from_user(user) }
+ let(:git_user) { Gitlab::Git::User.from_gitlab(user) }
- let(:commit_options) do
- author = repository.user_to_committer(user)
- { message: 'Test message', committer: author, author: author }
- end
+ let(:message) { 'Test message' }
let(:merge_commit) do
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
@@ -21,7 +18,7 @@ describe Repository, models: true do
merge_commit_id = repository.merge(user,
merge_request.diff_head_sha,
merge_request,
- commit_options)
+ message)
repository.commit(merge_commit_id)
end
@@ -886,7 +883,7 @@ describe Repository, models: true do
context 'when pre hooks were successful' do
it 'runs without errors' do
expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
- .with(committer, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature')
+ .with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature')
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
end
@@ -932,20 +929,20 @@ describe Repository, models: true do
service = Gitlab::Git::HooksService.new
expect(Gitlab::Git::HooksService).to receive(:new).and_return(service)
expect(service).to receive(:execute)
- .with(committer, target_repository.raw_repository, old_rev, new_rev, updating_ref)
+ .with(git_user, target_repository.raw_repository, old_rev, new_rev, updating_ref)
.and_yield(service).and_return(true)
end
it 'runs without errors' do
expect do
- Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do
+ Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do
new_rev
end
end.not_to raise_error
end
it 'ensures the autocrlf Git option is set to :input' do
- service = Gitlab::Git::OperationService.new(committer, repository.raw_repository)
+ service = Gitlab::Git::OperationService.new(git_user, repository.raw_repository)
expect(service).to receive(:update_autocrlf_option)
@@ -956,7 +953,7 @@ describe Repository, models: true do
it 'updates the head' do
expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev)
- Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do
+ Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do
new_rev
end
@@ -974,7 +971,7 @@ describe Repository, models: true do
expect(target_project.repository.raw_repository).to receive(:fetch_ref)
.and_call_original
- Gitlab::Git::OperationService.new(committer, target_repository.raw_repository)
+ Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository)
.with_branch(
'master',
start_repository: project.repository.raw_repository,
@@ -990,7 +987,7 @@ describe Repository, models: true do
it 'does not fetch_ref and just pass the commit' do
expect(target_repository).not_to receive(:fetch_ref)
- Gitlab::Git::OperationService.new(committer, target_repository.raw_repository)
+ Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository)
.with_branch('feature', start_repository: project.repository.raw_repository) { new_rev }
end
end
@@ -1009,7 +1006,7 @@ describe Repository, models: true do
end
expect do
- Gitlab::Git::OperationService.new(committer, target_project.repository.raw_repository)
+ Gitlab::Git::OperationService.new(git_user, target_project.repository.raw_repository)
.with_branch('feature',
start_repository: project.repository.raw_repository,
&:itself)
@@ -1031,7 +1028,7 @@ describe Repository, models: true do
repository.add_branch(user, branch, old_rev)
expect do
- Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do
+ Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do
new_rev
end
end.not_to raise_error
@@ -1049,7 +1046,7 @@ describe Repository, models: true do
# Updating 'master' to new_rev would lose the commits on 'master' that
# are not contained in new_rev. This should not be allowed.
expect do
- Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do
+ Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do
new_rev
end
end.to raise_error(Gitlab::Git::CommitError)
@@ -1061,7 +1058,7 @@ describe Repository, models: true do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
- Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do
+ Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do
new_rev
end
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
@@ -1287,10 +1284,7 @@ describe Repository, models: true do
describe '#merge' do
let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) }
- let(:commit_options) do
- author = repository.user_to_committer(user)
- { message: 'Test \r\n\r\n message', committer: author, author: author }
- end
+ let(:message) { 'Test \r\n\r\n message' }
it 'merges the code and returns the commit id' do
expect(merge_commit).to be_present
@@ -1298,19 +1292,19 @@ describe Repository, models: true do
end
it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
- merge_commit_id = merge(repository, user, merge_request, commit_options)
+ merge_commit_id = merge(repository, user, merge_request, message)
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
it 'removes carriage returns from commit message' do
- merge_commit_id = merge(repository, user, merge_request, commit_options)
+ merge_commit_id = merge(repository, user, merge_request, message)
- expect(repository.commit(merge_commit_id).message).to eq(commit_options[:message].delete("\r"))
+ expect(repository.commit(merge_commit_id).message).to eq(message.delete("\r"))
end
- def merge(repository, user, merge_request, options = {})
- repository.merge(user, merge_request.diff_head_sha, merge_request, options)
+ def merge(repository, user, merge_request, message)
+ repository.merge(user, merge_request.diff_head_sha, merge_request, message)
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 9602584f546..92e7d797cbd 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -54,9 +54,9 @@ describe API::Projects do
shared_examples_for 'projects response without N + 1 queries' do
it 'avoids N + 1 queries' do
- control_count = ActiveRecord::QueryRecorder.new do
+ control = ActiveRecord::QueryRecorder.new do
get api('/projects', current_user)
- end.count
+ end
if defined?(additional_project)
additional_project
@@ -66,7 +66,7 @@ describe API::Projects do
expect do
get api('/projects', current_user)
- end.not_to exceed_query_limit(control_count + 8)
+ end.not_to exceed_query_limit(control).with_threshold(8)
end
end
diff --git a/spec/services/deploy_keys/create_service_spec.rb b/spec/services/deploy_keys/create_service_spec.rb
new file mode 100644
index 00000000000..7a604c0cadd
--- /dev/null
+++ b/spec/services/deploy_keys/create_service_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe DeployKeys::CreateService do
+ let(:user) { create(:user) }
+ let(:params) { attributes_for(:deploy_key) }
+
+ subject { described_class.new(user, params) }
+
+ it "creates a deploy key" do
+ expect { subject.execute }.to change { DeployKey.where(params.merge(user: user)).count }.by(1)
+ end
+end
diff --git a/spec/services/gpg_keys/create_service_spec.rb b/spec/services/gpg_keys/create_service_spec.rb
new file mode 100644
index 00000000000..20382a3a618
--- /dev/null
+++ b/spec/services/gpg_keys/create_service_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe GpgKeys::CreateService do
+ let(:user) { create(:user) }
+ let(:params) { attributes_for(:gpg_key) }
+
+ subject { described_class.new(user, params) }
+
+ context 'notification', :mailer do
+ it 'sends a notification' do
+ perform_enqueued_jobs do
+ subject.execute
+ end
+ should_email(user)
+ end
+ end
+
+ it 'creates a gpg key' do
+ expect { subject.execute }.to change { user.gpg_keys.where(params).count }.by(1)
+ end
+end
diff --git a/spec/services/keys/create_service_spec.rb b/spec/services/keys/create_service_spec.rb
new file mode 100644
index 00000000000..bcb436c1e46
--- /dev/null
+++ b/spec/services/keys/create_service_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Keys::CreateService do
+ let(:user) { create(:user) }
+ let(:params) { attributes_for(:key) }
+
+ subject { described_class.new(user, params) }
+
+ context 'notification', :mailer do
+ it 'sends a notification' do
+ perform_enqueued_jobs do
+ subject.execute
+ end
+ should_email(user)
+ end
+ end
+
+ it 'creates a key' do
+ expect { subject.execute }.to change { user.keys.where(params).count }.by(1)
+ end
+end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 2af2485eeed..64e676f22a0 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -150,9 +150,7 @@ describe MergeRequests::RefreshService do
context 'manual merge of source branch' do
before do
# Merge master -> feature branch
- author = { email: 'test@gitlab.com', time: Time.now, name: "Me" }
- commit_options = { message: 'Test message', committer: author, author: author }
- @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, commit_options)
+ @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, 'Test message')
commit = @project.repository.commit('feature')
service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature')
reload_mrs
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 3e493148b32..f4b36eb7eeb 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -84,7 +84,6 @@ describe NotificationService, :mailer do
let!(:key) { create(:personal_key, key_options) }
it { expect(notification.new_key(key)).to be_truthy }
- it { should_email(key.user) }
describe 'never emails the ghost user' do
let(:key_options) { { user: User.ghost } }
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 92cc9a37795..c551083ac90 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -57,6 +57,21 @@ describe Projects::UpdateService, '#execute' do
end
end
end
+
+ context 'When project visibility is higher than parent group' do
+ let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
+
+ before do
+ project.update(namespace: group, visibility_level: group.visibility_level)
+ end
+
+ it 'does not update project visibility level' do
+ result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+
+ expect(result).to eq({ status: :error, message: 'Visibility level public is not allowed in a internal group.' })
+ expect(project.reload).to be_internal
+ end
+ end
end
describe 'when updating project that has forks' do
@@ -148,7 +163,7 @@ describe Projects::UpdateService, '#execute' do
result = update_project(project, admin, path: 'existing')
expect(result).to include(status: :error)
- expect(result[:message]).to match('Project could not be updated!')
+ expect(result[:message]).to match('There is already a repository with that name on disk')
expect(project).not_to be_valid
expect(project.errors.messages).to have_key(:base)
expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk')
@@ -159,8 +174,10 @@ describe Projects::UpdateService, '#execute' do
it 'returns an error result when record cannot be updated' do
result = update_project(project, admin, { name: 'foo&bar' })
- expect(result).to eq({ status: :error,
- message: 'Project could not be updated!' })
+ expect(result).to eq({
+ status: :error,
+ message: "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
+ })
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index ff1754fbe7e..92735336572 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,7 +1,7 @@
require './spec/simplecov_env'
SimpleCovEnv.start!
-ENV["RAILS_ENV"] ||= 'test'
+ENV["RAILS_ENV"] = 'test'
ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true'
require File.expand_path("../../config/environment", __FILE__)
diff --git a/spec/support/matchers/navigation_matcher.rb b/spec/support/matchers/navigation_matcher.rb
new file mode 100644
index 00000000000..5b6d9c1a4df
--- /dev/null
+++ b/spec/support/matchers/navigation_matcher.rb
@@ -0,0 +1,6 @@
+RSpec::Matchers.define :have_active_navigation do |expected|
+ match do |page|
+ expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
+ expect(page.find('.sidebar-top-level-items > li.active')).to have_content(expected)
+ end
+end
diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb
index 55b531b4cf7..ba0b805caad 100644
--- a/spec/support/query_recorder.rb
+++ b/spec/support/query_recorder.rb
@@ -34,15 +34,47 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
supports_block_expectations
match do |block|
- query_count(&block) > expected
+ query_count(&block) > expected_count + threshold
end
failure_message_when_negated do |actual|
- "Expected a maximum of #{expected} queries, got #{@recorder.count}:\n\n#{@recorder.log_message}"
+ threshold_message = threshold > 0 ? " (+#{@threshold})" : ''
+ counts = "#{expected_count}#{threshold_message}"
+ "Expected a maximum of #{counts} queries, got #{actual_count}:\n\n#{log_message}"
+ end
+
+ def with_threshold(threshold)
+ @threshold = threshold
+ self
+ end
+
+ def threshold
+ @threshold.to_i
+ end
+
+ def expected_count
+ if expected.is_a?(ActiveRecord::QueryRecorder)
+ expected.count
+ else
+ expected
+ end
+ end
+
+ def actual_count
+ @recorder.count
end
def query_count(&block)
@recorder = ActiveRecord::QueryRecorder.new(&block)
@recorder.count
end
+
+ def log_message
+ if expected.is_a?(ActiveRecord::QueryRecorder)
+ extra_queries = (expected.log - @recorder.log).join("\n\n")
+ "Extra queries: \n\n #{extra_queries}"
+ else
+ @recorder.log_message
+ end
+ end
end
diff --git a/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb
new file mode 100644
index 00000000000..c92c7f603d6
--- /dev/null
+++ b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb
@@ -0,0 +1,21 @@
+shared_examples 'issuable user dropdown behaviors' do
+ include FilteredSearchHelpers
+
+ before do
+ issuable # ensure we have at least one issuable
+ sign_in(user_in_dropdown)
+ end
+
+ %w[author assignee].each do |dropdown|
+ describe "#{dropdown} dropdown", :js do
+ it 'only includes members of the project/group' do
+ visit issuables_path
+
+ filtered_search.set("#{dropdown}:")
+
+ expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).to have_content(user_in_dropdown.name)
+ expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).not_to have_content(user_not_in_dropdown.name)
+ end
+ end
+ end
+end
diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
index 639b0924197..639b0924197 100644
--- a/spec/support/project_features_apply_to_issuables_shared_examples.rb
+++ b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
diff --git a/spec/support/shared_examples/features/search_shared_examples.rb b/spec/support/shared_examples/features/search_shared_examples.rb
new file mode 100644
index 00000000000..25ebbf011d5
--- /dev/null
+++ b/spec/support/shared_examples/features/search_shared_examples.rb
@@ -0,0 +1,5 @@
+shared_examples 'top right search form' do
+ it 'does not show top right search form' do
+ expect(page).not_to have_selector('.search')
+ end
+end
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb
index 9695f35bd25..78a2ff73746 100644
--- a/spec/support/stub_gitlab_calls.rb
+++ b/spec/support/stub_gitlab_calls.rb
@@ -26,11 +26,9 @@ module StubGitlabCalls
end
def stub_container_registry_config(registry_settings)
+ allow(Gitlab.config.registry).to receive_messages(registry_settings)
allow(Auth::ContainerRegistryAuthenticationService)
.to receive(:full_access_token).and_return('token')
-
- allow(Gitlab.config.registry).to receive_messages(registry_settings)
- load 'lib/gitlab/auth.rb'
end
def stub_container_registry_tags(repository: :any, tags:)
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 71b9deeabc3..6e5b9700b54 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -3,6 +3,8 @@ require 'rspec/mocks'
module TestEnv
extend self
+ ComponentFailedToInstallError = Class.new(StandardError)
+
# When developing the seed repository, comment out the branch you will modify.
BRANCH_SHA = {
'signed-commits' => '2d1096e',
@@ -63,6 +65,11 @@ module TestEnv
# See gitlab.yml.example test section for paths
#
def init(opts = {})
+ unless Rails.env.test?
+ puts "\nTestEnv.init can only be run if `RAILS_ENV` is set to 'test' not '#{Rails.env}'!\n"
+ exit 1
+ end
+
# Disable mailer for spinach tests
disable_mailer if opts[:mailer] == false
@@ -122,50 +129,23 @@ module TestEnv
end
def setup_gitlab_shell
- puts "\n==> Setting up Gitlab Shell..."
- start = Time.now
- gitlab_shell_dir = Gitlab.config.gitlab_shell.path
- shell_needs_update = component_needs_update?(gitlab_shell_dir,
- Gitlab::Shell.version_required)
-
- unless !shell_needs_update || system('rake', 'gitlab:shell:install')
- puts "\nGitLab Shell failed to install, cleaning up #{gitlab_shell_dir}!\n"
- FileUtils.rm_rf(gitlab_shell_dir)
- exit 1
- end
-
- puts " GitLab Shell setup in #{Time.now - start} seconds...\n"
+ component_timed_setup('GitLab Shell',
+ install_dir: Gitlab.config.gitlab_shell.path,
+ version: Gitlab::Shell.version_required,
+ task: 'gitlab:shell:install')
end
def setup_gitaly
- puts "\n==> Setting up Gitaly..."
- start = Time.now
socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '')
gitaly_dir = File.dirname(socket_path)
- if gitaly_dir_stale?(gitaly_dir)
- puts " Gitaly is outdated, cleaning up #{gitaly_dir}!"
- FileUtils.rm_rf(gitaly_dir)
- end
-
- gitaly_needs_update = component_needs_update?(gitaly_dir,
- Gitlab::GitalyClient.expected_server_version)
+ component_timed_setup('Gitaly',
+ install_dir: gitaly_dir,
+ version: Gitlab::GitalyClient.expected_server_version,
+ task: "gitlab:gitaly:install[#{gitaly_dir}]") do
- unless !gitaly_needs_update || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]")
- puts "\nGitaly failed to install, cleaning up #{gitaly_dir}!\n"
- FileUtils.rm_rf(gitaly_dir)
- exit 1
+ start_gitaly(gitaly_dir)
end
-
- start_gitaly(gitaly_dir)
- puts " Gitaly setup in #{Time.now - start} seconds...\n"
- end
-
- def gitaly_dir_stale?(dir)
- gitaly_executable = File.join(dir, 'gitaly')
- return false unless File.exist?(gitaly_executable)
-
- File.mtime(gitaly_executable) < File.mtime(Rails.root.join('GITALY_SERVER_VERSION'))
end
def start_gitaly(gitaly_dir)
@@ -320,6 +300,40 @@ module TestEnv
end
end
+ def component_timed_setup(component, install_dir:, version:, task:)
+ puts "\n==> Setting up #{component}..."
+ start = Time.now
+
+ ensure_component_dir_name_is_correct!(component, install_dir)
+
+ if component_needs_update?(install_dir, version)
+ # Cleanup the component entirely to ensure we start fresh
+ FileUtils.rm_rf(install_dir)
+ unless system('rake', task)
+ raise ComponentFailedToInstallError
+ end
+ end
+
+ yield if block_given?
+
+ rescue ComponentFailedToInstallError
+ puts "\n#{component} failed to install, cleaning up #{install_dir}!\n"
+ FileUtils.rm_rf(install_dir)
+ exit 1
+ ensure
+ puts " #{component} setup in #{Time.now - start} seconds...\n"
+ end
+
+ def ensure_component_dir_name_is_correct!(component, path)
+ actual_component_dir_name = File.basename(path)
+ expected_component_dir_name = component.parameterize
+
+ unless actual_component_dir_name == expected_component_dir_name
+ puts " #{component} install dir should be named '#{expected_component_dir_name}', not '#{actual_component_dir_name}' (full install path given was '#{path}')!\n"
+ exit 1
+ end
+ end
+
def component_needs_update?(component_folder, expected_version)
version = File.read(File.join(component_folder, 'VERSION')).strip
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index b17bc6692f3..c5f455b8948 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -1,16 +1,28 @@
require 'spec_helper'
describe 'layouts/nav/sidebar/_project' do
+ let(:project) { create(:project, :repository) }
+
+ before do
+ assign(:project, project)
+ assign(:repository, project.repository)
+ allow(view).to receive(:current_ref).and_return('master')
+
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ describe 'issue boards' do
+ it 'has boards tab when multiple issue boards available' do
+ render
+
+ expect(rendered).to have_css('a[title="Board"]')
+ end
+ end
+
describe 'container registry tab' do
before do
- project = create(:project, :repository)
stub_container_registry_config(enabled: true)
- assign(:project, project)
- assign(:repository, project.repository)
- allow(view).to receive(:current_ref).and_return('master')
-
- allow(view).to receive(:can?).and_return(true)
allow(controller).to receive(:controller_name)
.and_return('repositories')
allow(controller).to receive(:controller_path)
diff --git a/spec/views/shared/milestones/_issuable.html.haml.rb b/spec/views/shared/milestones/_issuable.html.haml.rb
new file mode 100644
index 00000000000..0a3f877cae0
--- /dev/null
+++ b/spec/views/shared/milestones/_issuable.html.haml.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe 'shared/milestones/_issuable.html.haml' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:issuable) { create(:issue, project: project, assignees: [user]) }
+
+ before do
+ assign(:project, project)
+ assign(:milestone, milestone)
+ end
+
+ it 'avatar links to issues page' do
+ render 'shared/milestones/issuable', issuable: issuable, show_project_name: true
+
+ expect(rendered).to have_css("a[href='#{project_issues_path(project, milestone_title: milestone.title, assignee_id: user.id, state: 'all')}']")
+ end
+end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 6f9ddb6c63c..f7b67b8efc6 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -31,8 +31,8 @@ describe GitGarbageCollectWorker do
expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
+ expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original
- expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
subject.perform(project.id, :gc, lease_key, lease_uuid)
end
@@ -77,8 +77,8 @@ describe GitGarbageCollectWorker do
expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
+ expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original
- expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
subject.perform(project.id)
end
diff --git a/yarn.lock b/yarn.lock
index de4a9ac4487..02917125f05 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -49,14 +49,7 @@ ajv-keywords@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0"
-ajv@^4.7.0:
- version "4.11.2"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.2.tgz#f166c3c11cbc6cb9dcc102a5bcfe5b72c95287e6"
- dependencies:
- co "^4.6.0"
- json-stable-stringify "^1.0.1"
-
-ajv@^4.9.1:
+ajv@^4.7.0, ajv@^4.9.1:
version "4.11.8"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
dependencies:
@@ -64,11 +57,11 @@ ajv@^4.9.1:
json-stable-stringify "^1.0.1"
ajv@^5.1.5:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.0.tgz#c1735024c5da2ef75cc190713073d44f098bf486"
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
dependencies:
co "^4.6.0"
- fast-deep-equal "^0.1.0"
+ fast-deep-equal "^1.0.0"
json-schema-traverse "^0.3.0"
json-stable-stringify "^1.0.1"
@@ -877,7 +870,7 @@ backo2@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
-balanced-match@^0.4.1, balanced-match@^0.4.2:
+balanced-match@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
@@ -983,14 +976,7 @@ bootstrap-sass@^3.3.6:
version "3.3.6"
resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.6.tgz#363b0d300e868d3e70134c1a742bb17288444fd1"
-brace-expansion@^1.0.0:
- version "1.1.6"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9"
- dependencies:
- balanced-match "^0.4.1"
- concat-map "0.0.1"
-
-brace-expansion@^1.1.8:
+brace-expansion@^1.0.0, brace-expansion@^1.1.7, brace-expansion@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
dependencies:
@@ -1712,7 +1698,7 @@ decompress-response@^3.2.0:
dependencies:
mimic-response "^1.0.0"
-deep-equal@^1.0.1:
+deep-equal@^1.0.1, deep-equal@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@@ -1730,7 +1716,14 @@ default-require-extensions@^1.0.0:
dependencies:
strip-bom "^2.0.0"
-defined@^1.0.0:
+define-properties@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94"
+ dependencies:
+ foreach "^2.0.5"
+ object-keys "^1.0.8"
+
+defined@^1.0.0, defined@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
@@ -2037,6 +2030,24 @@ error-ex@^1.2.0:
dependencies:
is-arrayish "^0.2.1"
+es-abstract@^1.5.0:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.8.2.tgz#25103263dc4decbda60e0c737ca32313518027ee"
+ dependencies:
+ es-to-primitive "^1.1.1"
+ function-bind "^1.1.1"
+ has "^1.0.1"
+ is-callable "^1.1.3"
+ is-regex "^1.0.4"
+
+es-to-primitive@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d"
+ dependencies:
+ is-callable "^1.1.1"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.1"
+
es5-ext@^0.10.14, es5-ext@^0.10.8, es5-ext@^0.10.9, es5-ext@~0.10.14, es5-ext@~0.10.2:
version "0.10.24"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.24.tgz#a55877c9924bc0c8d9bd3c2cbe17495ac1709b14"
@@ -2429,9 +2440,9 @@ extsprintf@1.3.0, extsprintf@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
-fast-deep-equal@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-0.1.0.tgz#5c6f4599aba6b333ee3342e2ed978672f1001f8d"
+fast-deep-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
fast-levenshtein@~2.0.4:
version "2.0.6"
@@ -2564,6 +2575,12 @@ follow-redirects@^1.2.3:
dependencies:
debug "^2.4.5"
+for-each@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4"
+ dependencies:
+ is-function "~1.0.0"
+
for-in@^0.1.5:
version "0.1.6"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8"
@@ -2574,6 +2591,10 @@ for-own@^0.1.4:
dependencies:
for-in "^0.1.5"
+foreach@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
+
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
@@ -2646,6 +2667,10 @@ function-bind@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771"
+function-bind@^1.1.1, function-bind@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@@ -2720,7 +2745,7 @@ glob@^6.0.4:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.0, glob@^7.1.1:
+glob@^7.0.0, glob@^7.1.1, glob@~7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
@@ -2742,11 +2767,7 @@ glob@^7.0.3, glob@^7.0.5:
once "^1.3.0"
path-is-absolute "^1.0.0"
-globals@^9.0.0:
- version "9.14.0"
- resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034"
-
-globals@^9.14.0:
+globals@^9.0.0, globals@^9.14.0:
version "9.18.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
@@ -2888,7 +2909,7 @@ has-unicode@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
-has@^1.0.1:
+has@^1.0.1, has@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
dependencies:
@@ -3163,6 +3184,14 @@ is-builtin-module@^1.0.0:
dependencies:
builtin-modules "^1.0.0"
+is-callable@^1.1.1, is-callable@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2"
+
+is-date-object@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
+
is-dotfile@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d"
@@ -3201,6 +3230,10 @@ is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+is-function@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5"
+
is-glob@^2.0.0, is-glob@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
@@ -3276,6 +3309,12 @@ is-redirect@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
+is-regex@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
+ dependencies:
+ has "^1.0.1"
+
is-relative@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.2.1.tgz#d27f4c7d516d175fb610db84bbeef23c3bc97aa5"
@@ -3302,6 +3341,10 @@ is-svg@^2.0.0:
dependencies:
html-comment-regex "^1.1.0"
+is-symbol@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572"
+
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -4087,7 +4130,7 @@ minimist@0.0.8, minimist@~0.0.1:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
-minimist@^1.1.3, minimist@^1.2.0:
+minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
@@ -4225,8 +4268,8 @@ node-libs-browser@^2.0.0:
vm-browserify "0.0.4"
node-pre-gyp@^0.6.36:
- version "0.6.36"
- resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786"
+ version "0.6.37"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.37.tgz#3c872b236b2e266e4140578fe1ee88f693323a05"
dependencies:
mkdirp "^0.5.1"
nopt "^4.0.1"
@@ -4235,6 +4278,7 @@ node-pre-gyp@^0.6.36:
request "^2.81.0"
rimraf "^2.6.1"
semver "^5.3.0"
+ tape "^4.6.3"
tar "^2.2.1"
tar-pack "^3.4.0"
@@ -4356,6 +4400,14 @@ object-component@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
+object-inspect@~1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.3.0.tgz#5b1eb8e6742e2ee83342a637034d844928ba2f6d"
+
+object-keys@^1.0.8:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
+
object.omit@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
@@ -5383,6 +5435,12 @@ resolve@^1.1.6, resolve@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c"
+resolve@~1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86"
+ dependencies:
+ path-parse "^1.0.5"
+
restore-cursor@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
@@ -5390,6 +5448,12 @@ restore-cursor@^1.0.1:
exit-hook "^1.0.0"
onetime "^1.0.0"
+resumer@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/resumer/-/resumer-0.0.0.tgz#f1e8f461e4064ba39e82af3cdc2a8c893d076759"
+ dependencies:
+ through "~2.3.4"
+
right-align@^0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
@@ -5805,6 +5869,14 @@ string-width@^2.0.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^3.0.0"
+string.prototype.trim@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.5.0"
+ function-bind "^1.0.2"
+
string_decoder@^0.10.25, string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
@@ -5896,6 +5968,24 @@ tapable@^0.2.7:
version "0.2.8"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22"
+tape@^4.6.3:
+ version "4.8.0"
+ resolved "https://registry.yarnpkg.com/tape/-/tape-4.8.0.tgz#f6a9fec41cc50a1de50fa33603ab580991f6068e"
+ dependencies:
+ deep-equal "~1.0.1"
+ defined "~1.0.0"
+ for-each "~0.3.2"
+ function-bind "~1.1.0"
+ glob "~7.1.2"
+ has "~1.0.1"
+ inherits "~2.0.3"
+ minimist "~1.2.0"
+ object-inspect "~1.3.0"
+ resolve "~1.4.0"
+ resumer "~0.0.0"
+ string.prototype.trim "~1.1.2"
+ through "~2.3.8"
+
tar-pack@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984"
@@ -5943,7 +6033,7 @@ three@^0.84.0:
version "0.84.0"
resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918"
-through@2, through@^2.3.6, through@~2.3, through@~2.3.1:
+through@2, through@^2.3.6, through@~2.3, through@~2.3.1, through@~2.3.4, through@~2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"