Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlejandro Rodríguez <alejorro70@gmail.com>2016-11-17 02:14:30 +0300
committerAlejandro Rodríguez <alejorro70@gmail.com>2016-11-17 02:14:30 +0300
commitbcd096f15bcb8edb47e7fe41f17870340fb1ceca (patch)
tree63aea6c338e3bda8e3d977ff2007cdc982db9b83
parente3e9cab007b977a373c155476cf30e863ac3aa17 (diff)
parent4d59a9e7687dce01d7c9e12c882a96832deff941 (diff)
Merge branch 'master' into 8-14-stable
-rw-r--r--.gitattributes1
-rw-r--r--CHANGELOG.md32
-rw-r--r--CONTRIBUTING.md6
-rw-r--r--app/assets/javascripts/application.js1
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es616
-rw-r--r--app/assets/javascripts/gl_dropdown.js2
-rw-r--r--app/assets/stylesheets/framework/avatar.scss7
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss8
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss20
-rw-r--r--app/assets/stylesheets/pages/search.scss6
-rw-r--r--app/controllers/projects/branches_controller.rb9
-rw-r--r--app/controllers/projects/lfs_api_controller.rb4
-rw-r--r--app/controllers/projects/services_controller.rb5
-rw-r--r--app/controllers/projects_controller.rb4
-rw-r--r--app/finders/labels_finder.rb47
-rw-r--r--app/helpers/events_helper.rb4
-rw-r--r--app/helpers/issuables_helper.rb16
-rw-r--r--app/helpers/lfs_helper.rb4
-rw-r--r--app/helpers/search_helper.rb29
-rw-r--r--app/helpers/triggers_helper.rb8
-rw-r--r--app/models/event.rb6
-rw-r--r--app/models/key.rb7
-rw-r--r--app/models/project.rb27
-rw-r--r--app/models/project_wiki.rb2
-rw-r--r--app/models/repository.rb31
-rw-r--r--app/models/service.rb4
-rw-r--r--app/models/tree.rb22
-rw-r--r--app/models/user.rb2
-rw-r--r--app/services/delete_merged_branches_service.rb18
-rw-r--r--app/services/merge_requests/refresh_service.rb21
-rw-r--r--app/services/projects/autocomplete_service.rb9
-rw-r--r--app/services/projects/create_service.rb9
-rw-r--r--app/services/projects/participants_service.rb9
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml4
-rw-r--r--app/views/projects/boards/components/_board.html.haml2
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/diffs/_content.html.haml2
-rw-r--r--app/views/projects/services/index.html.haml5
-rw-r--r--app/views/projects/triggers/index.html.haml20
-rw-r--r--app/views/search/results/_blob.html.haml12
-rw-r--r--app/views/shared/issuable/_form.html.haml68
-rw-r--r--app/views/shared/issuable/form/_title.html.haml32
-rw-r--r--app/workers/delete_merged_branches_worker.rb20
-rw-r--r--changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml4
-rw-r--r--changelogs/unreleased/21076-deleted-merged-branches.yml4
-rw-r--r--changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml4
-rw-r--r--changelogs/unreleased/21992-disable-access-requests-by-default.yml4
-rw-r--r--changelogs/unreleased/22588-todos-filter-shows-all-users.yml4
-rw-r--r--changelogs/unreleased/22680-unlabel-limit-autocomplete-to-selected-items.yml4
-rw-r--r--changelogs/unreleased/22790-mention-autocomplete-avatar.yml4
-rw-r--r--changelogs/unreleased/22947-fix_issues_atom_feed_url.yml4
-rw-r--r--changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml4
-rw-r--r--changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml4
-rw-r--r--changelogs/unreleased/23584-triggering-builds-from-webhooks.yml4
-rw-r--r--changelogs/unreleased/23731-add-param-to-user-api.yml4
-rw-r--r--changelogs/unreleased/23824-activity-page-does-not-show-commits-comments.yml4
-rw-r--r--changelogs/unreleased/23961-can-t-share-project-with-groups.yml4
-rw-r--r--changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml4
-rw-r--r--changelogs/unreleased/24059-round-robin-repository-storage.yml4
-rw-r--r--changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml4
-rw-r--r--changelogs/unreleased/24255-search-fix.yml4
-rw-r--r--changelogs/unreleased/24492-promise-polyfill.yml4
-rw-r--r--changelogs/unreleased/24496-fix-internal-api-project-lookup.yml4
-rw-r--r--changelogs/unreleased/adam-build-missing-services-when-necessary.yml4
-rw-r--r--changelogs/unreleased/add-api-label-id.yml4
-rw-r--r--changelogs/unreleased/add-project-import-data-index.yml4
-rw-r--r--changelogs/unreleased/api-label-priorities.yml4
-rw-r--r--changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml4
-rw-r--r--changelogs/unreleased/broken-link-frontend-dev-guide.yml4
-rw-r--r--changelogs/unreleased/faster_project_search.yml4
-rw-r--r--changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml4
-rw-r--r--changelogs/unreleased/fix-invalid-filename-eslint.yml4
-rw-r--r--changelogs/unreleased/fix-merge-request-screen-deleted-source-branch.yml4
-rw-r--r--changelogs/unreleased/fix-shibboleth-auth-with-no-uid.yml4
-rw-r--r--changelogs/unreleased/fix-uncheckable-label-for-force_remove_source_branch.yml4
-rw-r--r--changelogs/unreleased/fix_labels_api_adding_missing_parameter.yml4
-rw-r--r--changelogs/unreleased/fix_navigation_bar_issuables_counters.yml4
-rw-r--r--changelogs/unreleased/fix_saml_ldap_link.yml5
-rw-r--r--changelogs/unreleased/git-gc-improvements.yml4
-rw-r--r--changelogs/unreleased/issue-boards-counter-border-fix.yml4
-rw-r--r--changelogs/unreleased/issue_23032.yml4
-rw-r--r--changelogs/unreleased/ldap_check_bind.yml4
-rw-r--r--changelogs/unreleased/master-recursiveTree.yml4
-rw-r--r--changelogs/unreleased/pipeline-notifications.yml6
-rw-r--r--changelogs/unreleased/process-commits-using-sidekiq.yml4
-rw-r--r--changelogs/unreleased/remove-heading-space-from-diff-content.yml4
-rw-r--r--changelogs/unreleased/rs-issue-24527.yml4
-rw-r--r--changelogs/unreleased/setter-for-key.yml4
-rw-r--r--changelogs/unreleased/sh-bump-omniauth-gitlab.yml4
-rw-r--r--changelogs/unreleased/show-status-from-branch.yml4
-rw-r--r--changelogs/unreleased/sidekiq_default_retries.yml4
-rw-r--r--changelogs/unreleased/stanhu-gitlab-ce-fix-error-500-with-mr-images.yml4
-rw-r--r--changelogs/unreleased/upgrade-timeago.yml4
-rw-r--r--changelogs/unreleased/use-separate-token-for-incoming-email.yml4
-rw-r--r--changelogs/unreleased/user-dropdown-multiple-requests-fix.yml4
-rw-r--r--config/initializers/devise.rb4
-rw-r--r--config/routes/project.rb1
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--db/migrate/20161020075734_default_request_access_groups.rb12
-rw-r--r--db/migrate/20161020075830_default_request_access_projects.rb12
-rw-r--r--db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb46
-rw-r--r--db/schema.rb4
-rw-r--r--doc/administration/auth/ldap.md18
-rw-r--r--doc/administration/high_availability/README.md33
-rw-r--r--doc/administration/high_availability/redis.md945
-rw-r--r--doc/administration/high_availability/redis_source.md387
-rw-r--r--doc/api/branches.md18
-rw-r--r--doc/api/builds.md10
-rw-r--r--doc/api/repositories.md54
-rw-r--r--doc/ci/triggers/README.md24
-rw-r--r--doc/development/ux_guide/copy.md78
-rw-r--r--doc/development/ux_guide/img/copy-form-addissuebutton.pngbin0 -> 26541 bytes
-rw-r--r--doc/development/ux_guide/img/copy-form-addissueform.pngbin0 -> 38242 bytes
-rw-r--r--doc/development/ux_guide/img/copy-form-editissuebutton.pngbin0 -> 16466 bytes
-rw-r--r--doc/development/ux_guide/img/copy-form-editissueform.pngbin0 -> 40660 bytes
-rw-r--r--doc/development/ux_guide/index.md5
-rw-r--r--doc/integration/README.md6
-rw-r--r--features/snippet_search.feature20
-rw-r--r--features/snippets/discover.feature13
-rw-r--r--features/steps/shared/search.rb11
-rw-r--r--features/steps/snippet_search.rb55
-rw-r--r--features/steps/snippets/discover.rb21
-rw-r--r--lib/api/branches.rb12
-rw-r--r--lib/api/entities.rb15
-rw-r--r--lib/api/groups.rb156
-rw-r--r--lib/api/helpers.rb17
-rw-r--r--lib/api/helpers/internal_helpers.rb57
-rw-r--r--lib/api/internal.rb38
-rw-r--r--lib/api/merge_requests.rb272
-rw-r--r--lib/api/notes.rb124
-rw-r--r--lib/api/repositories.rb4
-rw-r--r--lib/api/triggers.rb2
-rw-r--r--lib/gitlab/diff/file.rb13
-rw-r--r--lib/gitlab/o_auth/user.rb2
-rw-r--r--lib/gitlab/project_search_results.rb51
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb2
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb58
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb2
-rw-r--r--spec/factories/groups.rb4
-rw-r--r--spec/factories/projects.rb4
-rw-r--r--spec/features/dashboard/issuables_counter_spec.rb44
-rw-r--r--spec/features/groups/members/owner_manages_access_requests_spec.rb2
-rw-r--r--spec/features/groups/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb10
-rw-r--r--spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb4
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb2
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/snippets/explore_spec.rb16
-rw-r--r--spec/features/snippets/search_snippets_spec.rb66
-rw-r--r--spec/finders/access_requests_finder_spec.rb15
-rw-r--r--spec/finders/labels_finder_spec.rb15
-rw-r--r--spec/helpers/members_helper_spec.rb8
-rw-r--r--spec/helpers/search_helper_spec.rb32
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb24
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb23
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb51
-rw-r--r--spec/mailers/notify_spec.rb15
-rw-r--r--spec/models/concerns/access_requestable_spec.rb8
-rw-r--r--spec/models/event_spec.rb27
-rw-r--r--spec/models/group_spec.rb2
-rw-r--r--spec/models/key_spec.rb22
-rw-r--r--spec/models/member_spec.rb4
-rw-r--r--spec/models/project_spec.rb8
-rw-r--r--spec/models/project_team_spec.rb8
-rw-r--r--spec/models/repository_spec.rb44
-rw-r--r--spec/models/service_spec.rb10
-rw-r--r--spec/models/user_spec.rb26
-rw-r--r--spec/requests/api/access_requests_spec.rb22
-rw-r--r--spec/requests/api/api_internal_helpers_spec.rb32
-rw-r--r--spec/requests/api/branches_spec.rb16
-rw-r--r--spec/requests/api/groups_spec.rb2
-rw-r--r--spec/requests/api/internal_spec.rb36
-rw-r--r--spec/requests/api/labels_spec.rb43
-rw-r--r--spec/requests/api/members_spec.rb20
-rw-r--r--spec/requests/api/merge_requests_spec.rb6
-rw-r--r--spec/requests/api/repositories_spec.rb35
-rw-r--r--spec/requests/api/services_spec.rb3
-rw-r--r--spec/requests/api/triggers_spec.rb7
-rw-r--r--spec/services/delete_merged_branches_service_spec.rb54
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb4
-rw-r--r--spec/services/members/destroy_service_spec.rb1
-rw-r--r--spec/services/members/request_access_service_spec.rb31
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb10
-rw-r--r--spec/services/projects/create_service_spec.rb20
-rw-r--r--spec/support/test_env.rb1
-rw-r--r--spec/workers/delete_merged_branches_worker_spec.rb19
-rw-r--r--vendor/assets/javascripts/es6-promise.auto.js1159
188 files changed, 4245 insertions, 1155 deletions
diff --git a/.gitattributes b/.gitattributes
index ab791a4cd6c..70cce05d2b5 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1 @@
-CHANGELOG.md merge=union
*.js.es6 gitlab-language=javascript
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 86a37d5bdb1..c35175b4bbc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,37 +4,6 @@ entry.
## 8.14.0 (2016-11-22)
-- Use separate email-token for incoming email and revert back the inactive feature. !5914
-- Replace jQuery.timeago with timeago.js. !6274 (ClemMakesApps)
-- Add CI notifications. Who triggered a pipeline would receive an email after the pipeline is succeeded or failed. Users could also update notification settings accordingly. !6342
-- Finer-grained Git gargage collection. !6588
-- Introduce better credential and error checking to `rake gitlab:ldap:check`. !6601
-- Process commits using a dedicated Sidekiq worker. !6802
-- Fix showing pipeline status for a given commit from correct branch. !7034
-- Add query param to filter users by external & blocked type. !7109 (Yatish Mehta)
-- Issues atom feed url reflect filters on dashboard. !7114 (Lucas Deschamps)
-- Add setting to only allow merge requests to be merged when all discussions are resolved. !7125 (Rodolfo Arruda)
-- Remove an extra leading space from diff paste data. !7133 (Hiroyuki Sato)
-- Fix 404 on network page when entering non-existent git revision. !7172 (Hiroyuki Sato)
-- Rewrite git blame spinach feature tests to rspec feature tests. !7197 (Lisanne Fellinger)
-- Only skip group when it's actually a group in the "Share with group" select. !7262
-- Introduce round-robin project creation to spread load over multiple shards. !7266
-- Ensure merge request's "remove branch" accessors return booleans. !7267
-- Expose label IDs in API. !7275 (Rares Sfirlogea)
-- Fix invalid filename validation on eslint. !7281
-- API: Ability to retrieve version information. !7286 (Robert Schilling)
-- Set default Sidekiq retries to 3. !7294
-- Return 400 when creating a system hook fails. !7350 (Robert Schilling)
-- Use the Gitlab Workhorse HTTP header in the admin dashboard. (Chris Wright)
-- Add an index for project_id in project_import_data to improve performance.
-- Fix broken link to observatory cli on Frontend Dev Guide. (Sam Rose)
-- Faster search inside Project.
-- Clicking "force remove source branch" label now toggles the checkbox again.
-- Allow to test JIRA service settings without having a repository.
-- Fix: Guest sees some repository details and gets 404.
-- Bump omniauth-gitlab to 1.0.2 to fix incompatibility with omniauth-oauth2.
-- Fix: Todos Filter Shows All Users.
-- Fix broken commits search.
- Show correct environment log in admin/logs (@duk3luk3 !7191)
- Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option !7117
- Diff collapse won't shift when collapsing.
@@ -104,6 +73,7 @@ entry.
- Fix applying GitHub-imported labels when importing job is interrupted
- Allow to search for user by secondary email address in the admin interface(/admin/users) !7115 (YarNayar)
- Updated commit SHA styling on the branches page.
+- Fix "Without projects" filter. !6611 (Ben Bodenmiller)
- Fix 404 when visit /projects page
## 8.13.5 (2016-11-08)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 67c30c2424c..6a009138446 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,8 +9,6 @@
- [Helping others](#helping-others)
- [I want to contribute!](#i-want-to-contribute)
- [Implement design & UI elements](#implement-design-ui-elements)
- - [Design reference](#design-reference)
- - [UI development kit](#ui-development-kit)
- [Issue tracker](#issue-tracker)
- [Feature proposals](#feature-proposals)
- [Issue tracker guidelines](#issue-tracker-guidelines)
@@ -90,7 +88,7 @@ This was inspired by [an article by Kent C. Dodds][medium-up-for-grabs].
## Implement design & UI elements
-Please see the [UI Guide for building GitLab].
+Please see the [UX Guide for GitLab].
## Issue tracker
@@ -469,5 +467,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
-[UI Guide for building GitLab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/ui_guide.md
+[UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/
[license-finder-doc]: doc/development/licensing.md
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 5c047dd4481..8d8431c424e 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -53,6 +53,7 @@
/*= require_directory ./u2f */
/*= require_directory . */
/*= require fuzzaldrin-plus */
+/*= require es6-promise.auto */
(function () {
document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch);
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index e72e2194be8..19bfdf1de8c 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -16,7 +16,7 @@
},
// Team Members
Members: {
- template: '<li>${username} <small>${title}</small></li>'
+ template: '<li>${avatarTag} ${username} <small>${title}</small></li>'
},
Labels: {
template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
@@ -51,6 +51,11 @@
if (!GitLab.GfmAutoComplete.dataLoaded) {
return this.at;
} else {
+ if (value.indexOf("unlabel") !== -1) {
+ GitLab.GfmAutoComplete.input.atwho('load', '~', GitLab.GfmAutoComplete.cachedData.unlabels);
+ } else {
+ GitLab.GfmAutoComplete.input.atwho('load', '~', GitLab.GfmAutoComplete.cachedData.labels);
+ }
return value;
}
}
@@ -118,7 +123,7 @@
beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(members) {
return $.map(members, function(m) {
- var title;
+ let title = '';
if (m.username == null) {
return m;
}
@@ -126,8 +131,14 @@
if (m.count) {
title += " (" + m.count + ")";
}
+
+ const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
+ const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`;
+ const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`;
+
return {
username: m.username,
+ avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
title: gl.utils.sanitize(title),
search: gl.utils.sanitize(m.username + " " + m.name)
};
@@ -352,3 +363,4 @@
};
}).call(this);
+
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 98e43c4d088..c016f25a7d3 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -249,7 +249,7 @@
_this.fullData = data;
_this.parseData(_this.fullData);
_this.focusTextInput();
- if (_this.options.filterable && _this.filter && _this.filter.input) {
+ if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input');
}
};
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 202ed5ae8fe..ad0d387067f 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -34,6 +34,7 @@
&.avatar-inline {
float: none;
+ display: inline-block;
margin-left: 4px;
margin-bottom: 2px;
@@ -41,6 +42,12 @@
&.s24 { margin-right: 4px; }
}
+ &.center {
+ font-size: 14px;
+ line-height: 1.8em;
+ text-align: center;
+ }
+
&.avatar-tile {
border-radius: 0;
border: none;
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index e7aff2d0cec..9acff45de75 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -141,6 +141,10 @@
&.btn-save {
@include btn-outline($white-light, $green-normal, $green-normal, $green-light, $white-light, $green-light);
}
+
+ &.btn-remove {
+ @include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light);
+ }
}
&.btn-gray {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 0e0673a72f6..16ecf466931 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -63,7 +63,11 @@ header {
&:focus,
&:active {
background-color: $background-color;
- color: darken($gl-icon-color, 50%);
+ color: darken($gl-icon-color, 30%);
+
+ .todos-pending-count {
+ background: darken($todo-alert-blue, 10%);
+ }
}
.fa-caret-down {
@@ -194,7 +198,7 @@ header {
cursor: pointer;
&:hover {
- color: darken($color: $gl-text-color, $amount: 50%);
+ color: darken($color: $gl-text-color, $amount: 30%);
}
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 6d28d98b283..8a93eac1b6d 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -148,7 +148,19 @@
}
}
-.atwho-view small.description {
- float: right;
- padding: 3px 5px;
-}
+.atwho-view {
+ small.description {
+ float: right;
+ padding: 3px 5px;
+ }
+
+ .avatar-inline {
+ margin-bottom: 0;
+ }
+
+ .cur {
+ .avatar {
+ border: 1px solid $white-light;
+ }
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 1a116f6a919..63d0a34e610 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -8,6 +8,10 @@
border-bottom: none;
}
}
+
+ .blob-result {
+ margin: 5px 0;
+ }
}
.search {
@@ -157,7 +161,6 @@
width: 68%;
}
}
-
}
.search-holder {
@@ -234,5 +237,4 @@
&:focus {
color: $gl-link-color;
}
-
}
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 2de8ada3e29..6b9f37983c4 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -4,7 +4,7 @@ class Projects::BranchesController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
- before_action :authorize_push_code!, only: [:new, :create, :destroy]
+ before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged]
def index
@sort = params[:sort].presence || sort_value_name
@@ -62,6 +62,13 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
+ def destroy_all_merged
+ DeleteMergedBranchesService.new(@project, current_user).async_execute
+
+ redirect_to namespace_project_branches_path(@project.namespace, @project),
+ notice: 'Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.'
+ end
+
private
def ref
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index ece49dcd922..2d493276941 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -31,10 +31,6 @@ class Projects::LfsApiController < Projects::GitHttpClientController
private
- def objects
- @objects ||= (params[:objects] || []).to_a
- end
-
def existing_oids
@existing_oids ||= begin
storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 97e6e9471e0..40a23a6f806 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -10,8 +10,7 @@ class Projects::ServicesController < Projects::ApplicationController
layout "project_settings"
def index
- @project.build_missing_services
- @services = @project.services.visible.reload
+ @services = @project.find_or_initialize_services
end
def edit
@@ -46,6 +45,6 @@ class Projects::ServicesController < Projects::ApplicationController
private
def service
- @service ||= @project.services.find { |service| service.to_param == params[:id] }
+ @service ||= @project.find_or_initialize_service(params[:id])
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a8a18b4fa16..7376c2bfeb7 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -144,13 +144,15 @@ class ProjectsController < Projects::ApplicationController
autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
+ unlabels = autocomplete.unlabels(noteable)
@suggestions = {
emojis: Gitlab::AwardEmoji.urls,
issues: autocomplete.issues,
milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests,
- labels: autocomplete.labels,
+ labels: autocomplete.labels - unlabels,
+ unlabels: unlabels,
members: participants,
commands: autocomplete.commands(noteable, params[:type])
}
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index 865f093f04a..fa0e2a5e3d8 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -6,7 +6,7 @@ class LabelsFinder < UnionFinder
def execute(skip_authorization: false)
@skip_authorization = skip_authorization
- items = find_union(label_ids, Label)
+ items = find_union(label_ids, Label) || Label.none
items = with_title(items)
sort(items)
end
@@ -18,9 +18,11 @@ class LabelsFinder < UnionFinder
def label_ids
label_ids = []
- if project
- label_ids << project.group.labels if project.group.present?
- label_ids << project.labels
+ if project?
+ if project
+ label_ids << project.group.labels if project.group.present?
+ label_ids << project.labels
+ end
else
label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id))
@@ -40,16 +42,16 @@ class LabelsFinder < UnionFinder
items.where(title: title)
end
- def group_id
- params[:group_id].presence
+ def group?
+ params[:group_id].present?
end
- def project_id
- params[:project_id].presence
+ def project?
+ params[:project_id].present?
end
- def projects_ids
- params[:project_ids]
+ def projects?
+ params[:project_ids].present?
end
def title
@@ -59,8 +61,9 @@ class LabelsFinder < UnionFinder
def project
return @project if defined?(@project)
- if project_id
- @project = find_project
+ if project?
+ @project = Project.find(params[:project_id])
+ @project = nil unless authorized_to_read_labels?(@project)
else
@project = nil
end
@@ -68,26 +71,20 @@ class LabelsFinder < UnionFinder
@project
end
- def find_project
- if skip_authorization
- Project.find_by(id: project_id)
- else
- available_projects.find_by(id: project_id)
- end
- end
-
def projects
return @projects if defined?(@projects)
- @projects = skip_authorization ? Project.all : available_projects
- @projects = @projects.in_namespace(group_id) if group_id
- @projects = @projects.where(id: projects_ids) if projects_ids
+ @projects = skip_authorization ? Project.all : ProjectsFinder.new.execute(current_user)
+ @projects = @projects.in_namespace(params[:group_id]) if group?
+ @projects = @projects.where(id: params[:project_ids]) if projects?
@projects = @projects.reorder(nil)
@projects
end
- def available_projects
- @available_projects ||= ProjectsFinder.new.execute(current_user)
+ def authorized_to_read_labels?(project)
+ return true if skip_authorization
+
+ Ability.allowed?(current_user, :read_label, project)
end
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 00e64076408..f1a0b929d82 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -86,7 +86,7 @@ module EventsHelper
elsif event.merge_request?
namespace_project_merge_request_url(event.project.namespace,
event.project, event.merge_request)
- elsif event.note? && event.commit_note?
+ elsif event.commit_note?
namespace_project_commit_url(event.project.namespace, event.project,
event.note_target)
elsif event.note?
@@ -127,7 +127,7 @@ module EventsHelper
end
def event_note_target_path(event)
- if event.note? && event.commit_note?
+ if event.commit_note?
namespace_project_commit_path(event.project.namespace,
event.project,
event.note_target,
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 8127c3f3ee3..ce2cabd7a3a 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -30,11 +30,6 @@ module IssuablesHelper
end
end
- def can_add_template?(issuable)
- names = issuable_templates(issuable)
- names.empty? && can?(current_user, :push_code, @project) && !@project.private?
- end
-
def template_dropdown_tag(issuable, &block)
title = selected_template(issuable) || "Choose a template"
options = {
@@ -141,8 +136,19 @@ module IssuablesHelper
html.html_safe
end
+ def cached_assigned_issuables_count(assignee, issuable_type, state)
+ cache_key = hexdigest(['assigned_issuables_count', assignee.id, issuable_type, state].join('-'))
+ Rails.cache.fetch(cache_key, expires_in: 2.minutes) do
+ assigned_issuables_count(assignee, issuable_type, state)
+ end
+ end
+
private
+ def assigned_issuables_count(assignee, issuable_type, state)
+ assignee.public_send("assigned_#{issuable_type}").public_send(state).count
+ end
+
def sidebar_gutter_collapsed?
cookies[:collapsed_gutter] == 'true'
end
diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb
index d3966ba1f10..2425c3a8bc8 100644
--- a/app/helpers/lfs_helper.rb
+++ b/app/helpers/lfs_helper.rb
@@ -30,6 +30,10 @@ module LfsHelper
ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
end
+ def objects
+ @objects ||= (params[:objects] || []).to_a
+ end
+
def user_can_download_code?
has_authentication_ability?(:download_code) && can?(user, :download_code, project)
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index aba3a3f9c5d..cdb9663877c 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -31,34 +31,7 @@ module SearchHelper
end
def parse_search_result(result)
- ref = nil
- filename = nil
- basename = nil
- startline = 0
-
- result.each_line.each_with_index do |line, index|
- if line =~ /^.*:.*:\d+:/
- ref, filename, startline = line.split(':')
- startline = startline.to_i - index
- extname = Regexp.escape(File.extname(filename))
- basename = filename.sub(/#{extname}$/, '')
- break
- end
- end
-
- data = ""
-
- result.each_line do |line|
- data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
- end
-
- OpenStruct.new(
- filename: filename,
- basename: basename,
- ref: ref,
- startline: startline,
- data: data
- )
+ Gitlab::ProjectSearchResults.parse_search_result(result)
end
private
diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb
index 8cad994d10f..c41181bab3d 100644
--- a/app/helpers/triggers_helper.rb
+++ b/app/helpers/triggers_helper.rb
@@ -1,5 +1,9 @@
module TriggersHelper
- def builds_trigger_url(project_id)
- "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds"
+ def builds_trigger_url(project_id, ref: nil)
+ if ref.nil?
+ "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds"
+ else
+ "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/ref/#{ref}/trigger/builds"
+ end
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index c76d88b1c7b..21eaca917b8 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -62,7 +62,7 @@ class Event < ActiveRecord::Base
end
def visible_to_user?(user = nil)
- if push?
+ if push? || commit_note?
Ability.allowed?(user, :download_code, project)
elsif membership_changed?
true
@@ -283,7 +283,7 @@ class Event < ActiveRecord::Base
end
def commit_note?
- target.for_commit?
+ note? && target && target.for_commit?
end
def issue_note?
@@ -295,7 +295,7 @@ class Event < ActiveRecord::Base
end
def project_snippet_note?
- target.for_snippet?
+ note? && target && target.for_snippet?
end
def note_target
diff --git a/app/models/key.rb b/app/models/key.rb
index 568a60b8af3..ff8dda2dc89 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -6,7 +6,7 @@ class Key < ActiveRecord::Base
belongs_to :user
- before_validation :strip_white_space, :generate_fingerprint
+ before_validation :generate_fingerprint
validates :title, presence: true, length: { within: 0..255 }
validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }
@@ -21,8 +21,9 @@ class Key < ActiveRecord::Base
after_destroy :remove_from_shell
after_destroy :post_destroy_hook
- def strip_white_space
- self.key = key.strip unless key.blank?
+ def key=(value)
+ value.strip! unless value.blank?
+ write_attribute(:key, value)
end
def publishable_key
diff --git a/app/models/project.rb b/app/models/project.rb
index 94aabafce20..4aedc91dc34 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -35,6 +35,7 @@ class Project < ActiveRecord::Base
default_value_for :builds_enabled, gitlab_config_features.builds
default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :snippets_enabled, gitlab_config_features.snippets
+ default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
after_create :ensure_dir_exist
after_create :create_project_feature, unless: :project_feature
@@ -76,7 +77,6 @@ class Project < ActiveRecord::Base
has_many :boards, before_add: :validate_board_limit, dependent: :destroy
# Project services
- has_many :services
has_one :campfire_service, dependent: :destroy
has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
@@ -748,27 +748,32 @@ class Project < ActiveRecord::Base
update_column(:has_external_wiki, services.external_wikis.any?)
end
- def build_missing_services
+ def find_or_initialize_services
services_templates = Service.where(template: true)
- Service.available_services_names.each do |service_name|
+ Service.available_services_names.map do |service_name|
service = find_service(services, service_name)
- # If service is available but missing in db
- if service.nil?
+ if service
+ service
+ else
# We should check if template for the service exists
template = find_service(services_templates, service_name)
if template.nil?
- # If no template, we should create an instance. Ex `create_gitlab_ci_service`
- public_send("create_#{service_name}_service")
+ # If no template, we should create an instance. Ex `build_gitlab_ci_service`
+ public_send("build_#{service_name}_service")
else
- Service.create_from_template(self.id, template)
+ Service.build_from_template(id, template)
end
end
end
end
+ def find_or_initialize_service(name)
+ find_or_initialize_services.find { |service| service.to_param == name }
+ end
+
def create_labels
Label.templates.each do |label|
params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
@@ -878,7 +883,7 @@ class Project < ActiveRecord::Base
end
def empty_repo?
- !repository.exists? || !repository.has_visible_content?
+ repository.empty_repo?
end
def repo
@@ -1334,10 +1339,6 @@ class Project < ActiveRecord::Base
end
end
- def only_allow_merge_if_all_discussions_are_resolved
- super || false
- end
-
private
def pushes_since_gc_redis_key
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 46f70da2452..9db96347322 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -127,7 +127,7 @@ class ProjectWiki
end
def search_files(query)
- repository.search_files(query, default_branch)
+ repository.search_files_by_content(query, default_branch)
end
def repository
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 063dc74021d..146424d2b1c 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -15,16 +15,6 @@ class Repository
Gitlab.config.repositories.storages
end
- def self.remove_storage_from_path(repo_path)
- storages.find do |_, storage_path|
- if repo_path.start_with?(storage_path)
- return repo_path.sub(storage_path, '')
- end
- end
-
- repo_path
- end
-
def initialize(path_with_namespace, project)
@path_with_namespace = path_with_namespace
@project = project
@@ -631,7 +621,7 @@ class Repository
@head_tree ||= Tree.new(self, head_commit.sha, nil)
end
- def tree(sha = :head, path = nil)
+ def tree(sha = :head, path = nil, recursive: false)
if sha == :head
if path.nil?
return head_tree
@@ -640,7 +630,7 @@ class Repository
end
end
- Tree.new(self, sha, path)
+ Tree.new(self, sha, path, recursive: recursive)
end
def blob_at_branch(branch_name, path)
@@ -1063,16 +1053,25 @@ class Repository
merge_base(ancestor_id, descendant_id) == ancestor_id
end
- def search_files(query, ref)
- unless exists? && has_visible_content? && query.present?
- return []
- end
+ def empty_repo?
+ !exists? || !has_visible_content?
+ end
+
+ def search_files_by_content(query, ref)
+ return [] if empty_repo? || query.blank?
offset = 2
args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
+ def search_files_by_name(query, ref)
+ return [] if empty_repo? || query.blank?
+
+ args = %W(#{Gitlab.config.git.bin_path} ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)})
+ Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip)
+ end
+
def fetch_ref(source_path, source_ref, target_ref)
args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
diff --git a/app/models/service.rb b/app/models/service.rb
index 625fbc48302..9d6ff190cdf 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -222,11 +222,11 @@ class Service < ActiveRecord::Base
]
end
- def self.create_from_template(project_id, template)
+ def self.build_from_template(project_id, template)
service = template.dup
service.template = false
service.project_id = project_id
- service if service.save
+ service
end
private
diff --git a/app/models/tree.rb b/app/models/tree.rb
index 7c4ed6e393b..2d1d68dbd81 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -3,15 +3,16 @@ class Tree
attr_accessor :repository, :sha, :path, :entries
- def initialize(repository, sha, path = '/')
+ def initialize(repository, sha, path = '/', recursive: false)
path = '/' if path.blank?
@repository = repository
@sha = sha
@path = path
+ @recursive = recursive
git_repo = @repository.raw_repository
- @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path)
+ @entries = get_entries(git_repo, @sha, @path, recursive: @recursive)
end
def readme
@@ -58,4 +59,21 @@ class Tree
def sorted_entries
trees + blobs + submodules
end
+
+ private
+
+ def get_entries(git_repo, sha, path, recursive: false)
+ current_path_entries = Gitlab::Git::Tree.where(git_repo, sha, path)
+ ordered_entries = []
+
+ current_path_entries.each do |entry|
+ ordered_entries << entry
+
+ if recursive && entry.dir?
+ ordered_entries.concat(get_entries(git_repo, sha, entry.path, recursive: true))
+ end
+ end
+
+ ordered_entries
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 3813df6684e..5a2b232c4ed 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -173,7 +173,7 @@ class User < ActiveRecord::Base
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
- scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
+ scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
def self.with_two_factor
diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb
new file mode 100644
index 00000000000..8b8deafedb7
--- /dev/null
+++ b/app/services/delete_merged_branches_service.rb
@@ -0,0 +1,18 @@
+require_relative 'base_service'
+
+class DeleteMergedBranchesService < BaseService
+ def async_execute
+ DeleteMergedBranchesWorker.perform_async(project.id, current_user.id)
+ end
+
+ def execute
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project)
+
+ branches = project.repository.branch_names
+ branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
+
+ branches.each do |branch|
+ DeleteBranchService.new(project, current_user).execute(branch)
+ end
+ end
+end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 22596b4014a..4a7e6930842 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -60,15 +60,7 @@ module MergeRequests
merge_requests = filter_merge_requests(merge_requests)
merge_requests.each do |merge_request|
- if merge_request.source_branch == @branch_name || force_push?
- merge_request.reload_diff
- else
- mr_commit_ids = merge_request.commits.map(&:id)
- push_commit_ids = @commits.map(&:id)
- matches = mr_commit_ids & push_commit_ids
- merge_request.reload_diff if matches.any?
- end
-
+ reload_diff(merge_request) unless branch_removed?
merge_request.mark_as_unchecked
end
end
@@ -173,5 +165,16 @@ module MergeRequests
def branch_removed?
Gitlab::Git.blank_ref?(@newrev)
end
+
+ def reload_diff(merge_request)
+ if merge_request.source_branch == @branch_name || force_push?
+ merge_request.reload_diff
+ else
+ mr_commit_ids = merge_request.commits.map(&:id)
+ push_commit_ids = @commits.map(&:id)
+ matches = mr_commit_ids & push_commit_ids
+ merge_request.reload_diff if matches.any?
+ end
+ end
end
end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 015f2828921..223461e88b6 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -13,7 +13,14 @@ module Projects
end
def labels
- LabelsFinder.new(current_user, project_id: project.id).execute.select([:title, :color])
+ LabelsFinder.new(current_user, project_id: project.id).execute.
+ pluck(:title, :color).map { |l| { title: l.first, color: l.second } }
+ end
+
+ def unlabels(noteable)
+ return [] unless noteable && noteable.respond_to?(:labels)
+
+ noteable.labels.pluck(:title, :color).map { |l| { title: l.first, color: l.second } }
end
def commands(noteable, type)
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 15d7918e7fd..28db145a1f4 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -95,7 +95,7 @@ module Projects
unless @project.gitlab_project_import?
@project.create_wiki unless skip_wiki?
- @project.build_missing_services
+ create_services_from_active_templates(@project)
@project.create_labels
end
@@ -135,5 +135,12 @@ module Projects
@project
end
+
+ def create_services_from_active_templates(project)
+ Service.where(template: true, active: true).each do |template|
+ service = Service.build_from_template(project.id, template)
+ service.save!
+ end
+ end
end
end
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index d38328403c1..6040391fd94 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -1,7 +1,7 @@
module Projects
class ParticipantsService < BaseService
attr_reader :noteable
-
+
def execute(noteable)
@noteable = noteable
@@ -15,7 +15,8 @@ module Projects
[{
name: noteable.author.name,
- username: noteable.author.username
+ username: noteable.author.username,
+ avatar_url: noteable.author.avatar_url
}]
end
@@ -28,14 +29,14 @@ module Projects
def sorted(users)
users.uniq.to_a.compact.sort_by(&:username).map do |user|
- { username: user.username, name: user.name }
+ { username: user.username, name: user.name, avatar_url: user.avatar_url }
end
end
def groups
current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count
- { username: group.path, name: group.name, count: count }
+ { username: group.path, name: group.name, count: count, avatar_url: group.avatar.url }
end
end
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index a0356feef95..2a6d9cda379 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -26,12 +26,12 @@
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
%span
Issues
- %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
+ %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
%span
Merge Requests
- %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
+ %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))
= nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, title: 'Snippets' do
%span
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index 47165c70097..a2e5118a9f3 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -7,7 +7,7 @@
data: { container: "body", placement: "bottom" } }
{{ list.title }}
.board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
- %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "done" }' }
+ %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "done" && !disabled }' }
{{ list.issuesSize }}
- if can?(current_user, :admin_issue, @project)
%button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 84f38575e84..2246316b540 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -26,6 +26,8 @@
= sort_title_oldest_updated
- if can? current_user, :push_code, @project
+ = link_to namespace_project_merged_branches_path(@project.namespace, @project), class: 'btn btn-inverted btn-remove has-tooltip', title: "Delete all branches that are merged into '#{@project.repository.root_ref}'", method: :delete, data: { confirm: "Deleting the merged branches cannot be undone. Are you sure?", container: 'body' } do
+ Delete merged branches
= link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do
New branch
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index c3d2f80544b..6120b2191dd 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -25,7 +25,7 @@
- elsif diff_file.renamed_file
.nothing-here-block File moved
- elsif blob.image?
- - old_blob = diff_file.old_blob(diff_commit)
+ - old_blob = diff_file.old_blob(diff_file.old_content_commit || @base_commit)
= render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob
- else
.nothing-here-block No preview for this file type
diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml
index 4a33a5bc6f6..66fd3029dc9 100644
--- a/app/views/projects/services/index.html.haml
+++ b/app/views/projects/services/index.html.haml
@@ -28,5 +28,6 @@
%td.hidden-xs
= service.description
%td.light
- = time_ago_in_words service.updated_at
- ago
+ - if service.updated_at.present?
+ = time_ago_in_words service.updated_at
+ ago
diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml
index f6e0b0a7c8a..6e5dd1b196d 100644
--- a/app/views/projects/triggers/index.html.haml
+++ b/app/views/projects/triggers/index.html.haml
@@ -76,6 +76,16 @@
script:
- "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}"
%h5.prepend-top-default
+ Use webhook
+
+ %p.light
+ Add the following webhook to another project for Push and Tag push events.
+ The project will be rebuilt at the corresponding event.
+
+ %pre
+ :plain
+ #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN
+ %h5.prepend-top-default
Pass build variables
%p.light
@@ -83,10 +93,18 @@
%code variables[VARIABLE]=VALUE
to an API request. Variable values can be used to distinguish between triggered builds and normal builds.
- %pre.append-bottom-0
+ With cURL:
+
+ %pre
:plain
curl -X POST \
-F token=TOKEN \
-F "ref=REF_NAME" \
-F "variables[RUN_NIGHTLY_BUILD]=true" \
#{builds_trigger_url(@project.id)}
+ %p.light
+ With webhook:
+
+ %pre.append-bottom-0
+ :plain
+ #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN&variables[RUN_NIGHTLY_BUILD]=true
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index 6f0a0ea36ec..9e8adc82583 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -1,11 +1,13 @@
-- blob = parse_search_result(blob)
+- file_name, blob = blob
.blob-result
.file-holder
.file-title
- - blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(blob.ref, blob.filename))
+ - ref = @search_results.repository_ref
+ - blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(ref, file_name))
= link_to blob_link do
%i.fa.fa-file
%strong
- = blob.filename
- .file-content.code.term
- = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
+ = file_name
+ - if blob
+ .file-content.code.term
+ = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 3176af9c19b..2fe9e82194b 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -1,3 +1,4 @@
+- form = local_assigns.fetch(:f)
- project = @target_project || @project
= form_errors(issuable)
@@ -10,44 +11,17 @@
and make sure your changes will not unintentionally remove theirs
.form-group
- = f.label :title, class: 'control-label'
+ = form.label :title, class: 'control-label'
= render 'shared/issuable/form/template_selector', issuable: issuable
-
- %div{ class: issuable_templates(issuable).any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' }
- = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
- class: 'form-control pad', required: true
-
- - if issuable.is_a?(MergeRequest)
- %p.help-block
- .js-wip-explanation
- %a.js-toggle-wip{href: "", tabindex: -1}
- Remove the
- %code WIP:
- prefix from the title
- to allow this
- %strong Work In Progress
- merge request to be merged when it's ready.
- .js-no-wip-explanation
- %a.js-toggle-wip{href: "", tabindex: -1}
- Start the title with
- %code WIP:
- to prevent a
- %strong Work In Progress
- merge request from being merged before it's ready.
-
- - if can_add_template?(issuable)
- %p.help-block
- Add
- = link_to "description templates", help_page_path('user/project/description_templates'), tabindex: -1
- to help your contributors communicate effectively!
+ = render 'shared/issuable/form/title', issuable: issuable, form: form
.form-group.detail-page-description
- = f.label :description, 'Description', class: 'control-label'
+ = form.label :description, 'Description', class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: f, attr: :description,
+ = render 'projects/zen', f: form, attr: :description,
classes: 'note-textarea',
placeholder: "Write a comment or drag your files here...",
supports_slash_commands: !issuable.persisted?
@@ -59,8 +33,8 @@
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
- = f.label :confidential do
- = f.check_box :confidential
+ = form.label :confidential do
+ = form.check_box :confidential
This issue is confidential and should only be visible to team members with at least Reporter access.
- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
@@ -69,32 +43,32 @@
.row
%div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
.form-group.issue-assignee
- = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
+ = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
- if issuable.assignee_id
- = f.hidden_field :assignee_id
+ = form.hidden_field :assignee_id
= dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
.form-group.issue-milestone
- = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
+ = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
= render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
.form-group
- has_labels = @labels && @labels.any?
- = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
- = f.hidden_field :label_ids, multiple: true, value: ''
+ = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
+ = form.hidden_field :label_ids, multiple: true, value: ''
.col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
.issuable-form-select-holder
= render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label"
- if has_due_date
.col-lg-6
.form-group
- = f.label :due_date, "Due date", class: "control-label"
+ = form.label :due_date, "Due date", class: "control-label"
.col-sm-10
.issuable-form-select-holder
- = f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
+ = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
- if issuable.can_move?(current_user)
%hr
@@ -112,15 +86,15 @@
%hr
- if @merge_request.new_record?
.form-group
- = f.label :source_branch, class: 'control-label'
+ = form.label :source_branch, class: 'control-label'
.col-sm-10
.issuable-form-select-holder
- = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
+ = form.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
.form-group
- = f.label :target_branch, class: 'control-label'
+ = form.label :target_branch, class: 'control-label'
.col-sm-10
.issuable-form-select-holder
- = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} })
+ = form.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} })
- if @merge_request.new_record?
&nbsp;
= link_to 'Change branches', mr_change_branches_path(@merge_request)
@@ -136,9 +110,9 @@
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
.row-content-block{class: (is_footer ? "footer-block" : "middle-block")}
- if issuable.new_record?
- = f.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
+ = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
- else
- = f.submit 'Save changes', class: 'btn btn-save'
+ = form.submit 'Save changes', class: 'btn btn-save'
- if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project))
.inline.prepend-left-10
@@ -155,4 +129,4 @@
method: :delete, class: 'btn btn-danger btn-grouped'
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
-= f.hidden_field :lock_version
+= form.hidden_field :lock_version
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
new file mode 100644
index 00000000000..83efdc7c8f7
--- /dev/null
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -0,0 +1,32 @@
+- issuable = local_assigns.fetch(:issuable)
+- form = local_assigns.fetch(:form)
+- no_issuable_templates = issuable_templates(issuable).empty?
+- div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8'
+
+%div{ class: div_class }
+ = form.text_field :title, required: true, maxlength: 255, autofocus: true,
+ autocomplete: 'off', class: 'form-control pad'
+
+ - if issuable.respond_to?(:work_in_progress?)
+ %p.help-block
+ .js-wip-explanation
+ %a.js-toggle-wip{ href: '', tabindex: -1 }
+ Remove the
+ %code WIP:
+ prefix from the title
+ to allow this
+ %strong Work In Progress
+ merge request to be merged when it's ready.
+ .js-no-wip-explanation
+ %a.js-toggle-wip{ href: '', tabindex: -1 }
+ Start the title with
+ %code WIP:
+ to prevent a
+ %strong Work In Progress
+ merge request from being merged before it's ready.
+
+ - if no_issuable_templates && can?(current_user, :push_code, issuable.project)
+ %p.help-block
+ Add
+ = link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1
+ to help your contributors communicate effectively!
diff --git a/app/workers/delete_merged_branches_worker.rb b/app/workers/delete_merged_branches_worker.rb
new file mode 100644
index 00000000000..f870da4ecfd
--- /dev/null
+++ b/app/workers/delete_merged_branches_worker.rb
@@ -0,0 +1,20 @@
+class DeleteMergedBranchesWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(project_id, user_id)
+ begin
+ project = Project.find(project_id)
+ rescue ActiveRecord::RecordNotFound
+ return
+ end
+
+ user = User.find(user_id)
+
+ begin
+ DeleteMergedBranchesService.new(project, user).execute
+ rescue Gitlab::Access::AccessDeniedError
+ return
+ end
+ end
+end
diff --git a/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml b/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml
new file mode 100644
index 00000000000..8f03746ff80
--- /dev/null
+++ b/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml
@@ -0,0 +1,4 @@
+---
+title: Add setting to only allow merge requests to be merged when all discussions are resolved
+merge_request: 7125
+author: Rodolfo Arruda
diff --git a/changelogs/unreleased/21076-deleted-merged-branches.yml b/changelogs/unreleased/21076-deleted-merged-branches.yml
new file mode 100644
index 00000000000..b7fa7f14384
--- /dev/null
+++ b/changelogs/unreleased/21076-deleted-merged-branches.yml
@@ -0,0 +1,4 @@
+---
+title: Add button to delete all merged branches
+merge_request: 6449
+author: Toon Claes
diff --git a/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml b/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml
new file mode 100644
index 00000000000..95d8fef1099
--- /dev/null
+++ b/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml
@@ -0,0 +1,4 @@
+---
+title: Use the Gitlab Workhorse HTTP header in the admin dashboard
+merge_request:
+author: Chris Wright
diff --git a/changelogs/unreleased/21992-disable-access-requests-by-default.yml b/changelogs/unreleased/21992-disable-access-requests-by-default.yml
new file mode 100644
index 00000000000..ddcb2169407
--- /dev/null
+++ b/changelogs/unreleased/21992-disable-access-requests-by-default.yml
@@ -0,0 +1,4 @@
+---
+title: Disable "Request Access" functionality by default for new projects and groups
+merge_request: 7425
+author:
diff --git a/changelogs/unreleased/22588-todos-filter-shows-all-users.yml b/changelogs/unreleased/22588-todos-filter-shows-all-users.yml
new file mode 100644
index 00000000000..1da72142880
--- /dev/null
+++ b/changelogs/unreleased/22588-todos-filter-shows-all-users.yml
@@ -0,0 +1,4 @@
+---
+title: 'Fix: Todos Filter Shows All Users'
+merge_request:
+author:
diff --git a/changelogs/unreleased/22680-unlabel-limit-autocomplete-to-selected-items.yml b/changelogs/unreleased/22680-unlabel-limit-autocomplete-to-selected-items.yml
new file mode 100644
index 00000000000..95fd07c12e1
--- /dev/null
+++ b/changelogs/unreleased/22680-unlabel-limit-autocomplete-to-selected-items.yml
@@ -0,0 +1,4 @@
+---
+title: Limit autocomplete to currently selected items for unlabel slash command
+merge_request: 22680
+author: Akram Fares
diff --git a/changelogs/unreleased/22790-mention-autocomplete-avatar.yml b/changelogs/unreleased/22790-mention-autocomplete-avatar.yml
new file mode 100644
index 00000000000..53068ca5607
--- /dev/null
+++ b/changelogs/unreleased/22790-mention-autocomplete-avatar.yml
@@ -0,0 +1,4 @@
+---
+title: Show avatars in mention dropdown
+merge_request: 6865
+author:
diff --git a/changelogs/unreleased/22947-fix_issues_atom_feed_url.yml b/changelogs/unreleased/22947-fix_issues_atom_feed_url.yml
new file mode 100644
index 00000000000..2312afdb3d7
--- /dev/null
+++ b/changelogs/unreleased/22947-fix_issues_atom_feed_url.yml
@@ -0,0 +1,4 @@
+---
+title: Issues atom feed url reflect filters on dashboard
+merge_request: 7114
+author: Lucas Deschamps
diff --git a/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml b/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml
new file mode 100644
index 00000000000..7b54d3df56d
--- /dev/null
+++ b/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml
@@ -0,0 +1,4 @@
+---
+title: Rewrite git blame spinach feature tests to rspec feature tests
+merge_request: 7197
+author: Lisanne Fellinger
diff --git a/changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml b/changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml
new file mode 100644
index 00000000000..156f8d779ca
--- /dev/null
+++ b/changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml
@@ -0,0 +1,4 @@
+---
+title: Search for a filename in a project
+merge_request:
+author:
diff --git a/changelogs/unreleased/23584-triggering-builds-from-webhooks.yml b/changelogs/unreleased/23584-triggering-builds-from-webhooks.yml
new file mode 100644
index 00000000000..59e0d851366
--- /dev/null
+++ b/changelogs/unreleased/23584-triggering-builds-from-webhooks.yml
@@ -0,0 +1,4 @@
+---
+title: Make it possible to trigger builds from webhooks
+merge_request: 7022
+author: Dmitry Poray
diff --git a/changelogs/unreleased/23731-add-param-to-user-api.yml b/changelogs/unreleased/23731-add-param-to-user-api.yml
new file mode 100644
index 00000000000..e31029ffb27
--- /dev/null
+++ b/changelogs/unreleased/23731-add-param-to-user-api.yml
@@ -0,0 +1,4 @@
+---
+title: Add query param to filter users by external & blocked type
+merge_request: 7109
+author: Yatish Mehta
diff --git a/changelogs/unreleased/23824-activity-page-does-not-show-commits-comments.yml b/changelogs/unreleased/23824-activity-page-does-not-show-commits-comments.yml
new file mode 100644
index 00000000000..48f733f9c5e
--- /dev/null
+++ b/changelogs/unreleased/23824-activity-page-does-not-show-commits-comments.yml
@@ -0,0 +1,4 @@
+---
+title: Allow commit note to be visible if repo is visible
+merge_request:
+author:
diff --git a/changelogs/unreleased/23961-can-t-share-project-with-groups.yml b/changelogs/unreleased/23961-can-t-share-project-with-groups.yml
new file mode 100644
index 00000000000..b3bfcbda4b7
--- /dev/null
+++ b/changelogs/unreleased/23961-can-t-share-project-with-groups.yml
@@ -0,0 +1,4 @@
+---
+title: Only skip group when it's actually a group in the "Share with group" select
+merge_request: 7262
+author:
diff --git a/changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml b/changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml
new file mode 100644
index 00000000000..8ca0c5beab3
--- /dev/null
+++ b/changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml
@@ -0,0 +1,4 @@
+---
+title: 'Fix: Guest sees some repository details and gets 404'
+merge_request:
+author:
diff --git a/changelogs/unreleased/24059-round-robin-repository-storage.yml b/changelogs/unreleased/24059-round-robin-repository-storage.yml
new file mode 100644
index 00000000000..109536114ff
--- /dev/null
+++ b/changelogs/unreleased/24059-round-robin-repository-storage.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce round-robin project creation to spread load over multiple shards
+merge_request: 7266
+author:
diff --git a/changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml b/changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml
new file mode 100644
index 00000000000..50d018170f1
--- /dev/null
+++ b/changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml
@@ -0,0 +1,4 @@
+---
+title: Ensure merge request's "remove branch" accessors return booleans
+merge_request: 7267
+author:
diff --git a/changelogs/unreleased/24255-search-fix.yml b/changelogs/unreleased/24255-search-fix.yml
new file mode 100644
index 00000000000..c0afade9bc8
--- /dev/null
+++ b/changelogs/unreleased/24255-search-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fix broken commits search
+merge_request:
+author:
diff --git a/changelogs/unreleased/24492-promise-polyfill.yml b/changelogs/unreleased/24492-promise-polyfill.yml
new file mode 100644
index 00000000000..d2fddfd83c6
--- /dev/null
+++ b/changelogs/unreleased/24492-promise-polyfill.yml
@@ -0,0 +1,4 @@
+---
+title: Adds es6-promise Polyfill
+merge_request: 7482
+author:
diff --git a/changelogs/unreleased/24496-fix-internal-api-project-lookup.yml b/changelogs/unreleased/24496-fix-internal-api-project-lookup.yml
new file mode 100644
index 00000000000..a95295c00f3
--- /dev/null
+++ b/changelogs/unreleased/24496-fix-internal-api-project-lookup.yml
@@ -0,0 +1,4 @@
+---
+title: Fix POST /internal/allowed to cope with gitlab-shell v4.0.0 project paths
+merge_request: 7480
+author:
diff --git a/changelogs/unreleased/adam-build-missing-services-when-necessary.yml b/changelogs/unreleased/adam-build-missing-services-when-necessary.yml
new file mode 100644
index 00000000000..8b157e31e99
--- /dev/null
+++ b/changelogs/unreleased/adam-build-missing-services-when-necessary.yml
@@ -0,0 +1,4 @@
+---
+title: Defer saving project services to the database if there are no user changes
+merge_request: 6958
+author:
diff --git a/changelogs/unreleased/add-api-label-id.yml b/changelogs/unreleased/add-api-label-id.yml
new file mode 100644
index 00000000000..3af4f5e677d
--- /dev/null
+++ b/changelogs/unreleased/add-api-label-id.yml
@@ -0,0 +1,4 @@
+---
+title: Expose label IDs in API
+merge_request: 7275
+author: Rares Sfirlogea
diff --git a/changelogs/unreleased/add-project-import-data-index.yml b/changelogs/unreleased/add-project-import-data-index.yml
new file mode 100644
index 00000000000..f5e4005f544
--- /dev/null
+++ b/changelogs/unreleased/add-project-import-data-index.yml
@@ -0,0 +1,4 @@
+---
+title: Add an index for project_id in project_import_data to improve performance
+merge_request:
+author:
diff --git a/changelogs/unreleased/api-label-priorities.yml b/changelogs/unreleased/api-label-priorities.yml
new file mode 100644
index 00000000000..d703f8d32f3
--- /dev/null
+++ b/changelogs/unreleased/api-label-priorities.yml
@@ -0,0 +1,4 @@
+---
+title: "API: Ability to retrieve version information"
+merge_request: 7286
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml b/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml
new file mode 100644
index 00000000000..d132d7e79c3
--- /dev/null
+++ b/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml
@@ -0,0 +1,4 @@
+---
+title: Return 400 when creating a system hook fails
+merge_request: 7350
+author: Robert Schilling
diff --git a/changelogs/unreleased/broken-link-frontend-dev-guide.yml b/changelogs/unreleased/broken-link-frontend-dev-guide.yml
new file mode 100644
index 00000000000..d7b6f4a7701
--- /dev/null
+++ b/changelogs/unreleased/broken-link-frontend-dev-guide.yml
@@ -0,0 +1,4 @@
+---
+title: Fix broken link to observatory cli on Frontend Dev Guide
+merge_request:
+author: Sam Rose
diff --git a/changelogs/unreleased/faster_project_search.yml b/changelogs/unreleased/faster_project_search.yml
new file mode 100644
index 00000000000..e29a9f34ed4
--- /dev/null
+++ b/changelogs/unreleased/faster_project_search.yml
@@ -0,0 +1,4 @@
+---
+title: Faster search inside Project
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml b/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml
new file mode 100644
index 00000000000..d1bc8ea2eb1
--- /dev/null
+++ b/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml
@@ -0,0 +1,4 @@
+---
+title: Fix 404 on network page when entering non-existent git revision
+merge_request: 7172
+author: Hiroyuki Sato
diff --git a/changelogs/unreleased/fix-invalid-filename-eslint.yml b/changelogs/unreleased/fix-invalid-filename-eslint.yml
new file mode 100644
index 00000000000..eea21149c90
--- /dev/null
+++ b/changelogs/unreleased/fix-invalid-filename-eslint.yml
@@ -0,0 +1,4 @@
+---
+title: Fix invalid filename validation on eslint
+merge_request: 7281
+author:
diff --git a/changelogs/unreleased/fix-merge-request-screen-deleted-source-branch.yml b/changelogs/unreleased/fix-merge-request-screen-deleted-source-branch.yml
new file mode 100644
index 00000000000..a6bee989f6d
--- /dev/null
+++ b/changelogs/unreleased/fix-merge-request-screen-deleted-source-branch.yml
@@ -0,0 +1,4 @@
+---
+title: Do not create a MergeRequestDiff record when source branch is deleted
+merge_request: 7481
+author:
diff --git a/changelogs/unreleased/fix-shibboleth-auth-with-no-uid.yml b/changelogs/unreleased/fix-shibboleth-auth-with-no-uid.yml
new file mode 100644
index 00000000000..56fa2170be3
--- /dev/null
+++ b/changelogs/unreleased/fix-shibboleth-auth-with-no-uid.yml
@@ -0,0 +1,4 @@
+---
+title: fix shibboleth misconfigurations resulting in authentication bypass
+merge_request: 7428
+author:
diff --git a/changelogs/unreleased/fix-uncheckable-label-for-force_remove_source_branch.yml b/changelogs/unreleased/fix-uncheckable-label-for-force_remove_source_branch.yml
new file mode 100644
index 00000000000..8b41063151b
--- /dev/null
+++ b/changelogs/unreleased/fix-uncheckable-label-for-force_remove_source_branch.yml
@@ -0,0 +1,4 @@
+---
+title: Clicking "force remove source branch" label now toggles the checkbox again
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix_labels_api_adding_missing_parameter.yml b/changelogs/unreleased/fix_labels_api_adding_missing_parameter.yml
new file mode 100644
index 00000000000..01b191a8c5a
--- /dev/null
+++ b/changelogs/unreleased/fix_labels_api_adding_missing_parameter.yml
@@ -0,0 +1,4 @@
+---
+title: Fix labels API by adding missing current_user parameter
+merge_request: 7458
+author: Francesco Coda Zabetta
diff --git a/changelogs/unreleased/fix_navigation_bar_issuables_counters.yml b/changelogs/unreleased/fix_navigation_bar_issuables_counters.yml
new file mode 100644
index 00000000000..0f7f8155f91
--- /dev/null
+++ b/changelogs/unreleased/fix_navigation_bar_issuables_counters.yml
@@ -0,0 +1,4 @@
+---
+title: Navigation bar issuables counters reflects dashboard issuables counters
+merge_request: 7368
+author: Lucas Deschamps
diff --git a/changelogs/unreleased/fix_saml_ldap_link.yml b/changelogs/unreleased/fix_saml_ldap_link.yml
new file mode 100644
index 00000000000..3b6f26d610e
--- /dev/null
+++ b/changelogs/unreleased/fix_saml_ldap_link.yml
@@ -0,0 +1,5 @@
+---
+title: Omniauth auto link LDAP user falls back to find by DN when user cannot be found
+ by UID
+merge_request: 7002
+author:
diff --git a/changelogs/unreleased/git-gc-improvements.yml b/changelogs/unreleased/git-gc-improvements.yml
new file mode 100644
index 00000000000..f15e667ce87
--- /dev/null
+++ b/changelogs/unreleased/git-gc-improvements.yml
@@ -0,0 +1,4 @@
+---
+title: Finer-grained Git gargage collection
+merge_request: 6588
+author:
diff --git a/changelogs/unreleased/issue-boards-counter-border-fix.yml b/changelogs/unreleased/issue-boards-counter-border-fix.yml
new file mode 100644
index 00000000000..c98adb6af7c
--- /dev/null
+++ b/changelogs/unreleased/issue-boards-counter-border-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed issue boards counter border when unauthorized
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_23032.yml b/changelogs/unreleased/issue_23032.yml
new file mode 100644
index 00000000000..d376cf52112
--- /dev/null
+++ b/changelogs/unreleased/issue_23032.yml
@@ -0,0 +1,4 @@
+---
+title: Allow to test JIRA service settings without having a repository
+merge_request:
+author:
diff --git a/changelogs/unreleased/ldap_check_bind.yml b/changelogs/unreleased/ldap_check_bind.yml
new file mode 100644
index 00000000000..daff8103a07
--- /dev/null
+++ b/changelogs/unreleased/ldap_check_bind.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce better credential and error checking to `rake gitlab:ldap:check`
+merge_request: 6601
+author:
diff --git a/changelogs/unreleased/master-recursiveTree.yml b/changelogs/unreleased/master-recursiveTree.yml
new file mode 100644
index 00000000000..c6384d172e2
--- /dev/null
+++ b/changelogs/unreleased/master-recursiveTree.yml
@@ -0,0 +1,4 @@
+---
+title: API: allow recursive tree request
+merge_request: 6088
+author: Rebeca Méndez
diff --git a/changelogs/unreleased/pipeline-notifications.yml b/changelogs/unreleased/pipeline-notifications.yml
new file mode 100644
index 00000000000..b43060674b2
--- /dev/null
+++ b/changelogs/unreleased/pipeline-notifications.yml
@@ -0,0 +1,6 @@
+---
+title: Add CI notifications. Who triggered a pipeline would receive an email after
+ the pipeline is succeeded or failed. Users could also update notification settings
+ accordingly
+merge_request: 6342
+author:
diff --git a/changelogs/unreleased/process-commits-using-sidekiq.yml b/changelogs/unreleased/process-commits-using-sidekiq.yml
new file mode 100644
index 00000000000..9f596e6a584
--- /dev/null
+++ b/changelogs/unreleased/process-commits-using-sidekiq.yml
@@ -0,0 +1,4 @@
+---
+title: Process commits using a dedicated Sidekiq worker
+merge_request: 6802
+author:
diff --git a/changelogs/unreleased/remove-heading-space-from-diff-content.yml b/changelogs/unreleased/remove-heading-space-from-diff-content.yml
new file mode 100644
index 00000000000..1ea85784d29
--- /dev/null
+++ b/changelogs/unreleased/remove-heading-space-from-diff-content.yml
@@ -0,0 +1,4 @@
+---
+title: Remove an extra leading space from diff paste data
+merge_request: 7133
+author: Hiroyuki Sato
diff --git a/changelogs/unreleased/rs-issue-24527.yml b/changelogs/unreleased/rs-issue-24527.yml
new file mode 100644
index 00000000000..a7b6358e60e
--- /dev/null
+++ b/changelogs/unreleased/rs-issue-24527.yml
@@ -0,0 +1,4 @@
+---
+title: Limit labels returned for a specific project as an administrator
+merge_request: 7496
+author:
diff --git a/changelogs/unreleased/setter-for-key.yml b/changelogs/unreleased/setter-for-key.yml
new file mode 100644
index 00000000000..15167904ed5
--- /dev/null
+++ b/changelogs/unreleased/setter-for-key.yml
@@ -0,0 +1,4 @@
+---
+title: Use setter for key instead AR callback
+merge_request: 7488
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/sh-bump-omniauth-gitlab.yml b/changelogs/unreleased/sh-bump-omniauth-gitlab.yml
new file mode 100644
index 00000000000..17cd5a993dd
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-omniauth-gitlab.yml
@@ -0,0 +1,4 @@
+---
+title: Bump omniauth-gitlab to 1.0.2 to fix incompatibility with omniauth-oauth2
+merge_request:
+author:
diff --git a/changelogs/unreleased/show-status-from-branch.yml b/changelogs/unreleased/show-status-from-branch.yml
new file mode 100644
index 00000000000..1afc230c05c
--- /dev/null
+++ b/changelogs/unreleased/show-status-from-branch.yml
@@ -0,0 +1,4 @@
+---
+title: Fix showing pipeline status for a given commit from correct branch
+merge_request: 7034
+author:
diff --git a/changelogs/unreleased/sidekiq_default_retries.yml b/changelogs/unreleased/sidekiq_default_retries.yml
new file mode 100644
index 00000000000..3df2a415dbc
--- /dev/null
+++ b/changelogs/unreleased/sidekiq_default_retries.yml
@@ -0,0 +1,4 @@
+---
+title: Set default Sidekiq retries to 3
+merge_request: 7294
+author:
diff --git a/changelogs/unreleased/stanhu-gitlab-ce-fix-error-500-with-mr-images.yml b/changelogs/unreleased/stanhu-gitlab-ce-fix-error-500-with-mr-images.yml
new file mode 100644
index 00000000000..7ca0d5fb19e
--- /dev/null
+++ b/changelogs/unreleased/stanhu-gitlab-ce-fix-error-500-with-mr-images.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Error 500 when creating a merge request that contains an image that was deleted and added
+merge_request: 7457
+author:
diff --git a/changelogs/unreleased/upgrade-timeago.yml b/changelogs/unreleased/upgrade-timeago.yml
new file mode 100644
index 00000000000..ddb266ba558
--- /dev/null
+++ b/changelogs/unreleased/upgrade-timeago.yml
@@ -0,0 +1,4 @@
+---
+title: Replace jQuery.timeago with timeago.js
+merge_request: 6274
+author: ClemMakesApps
diff --git a/changelogs/unreleased/use-separate-token-for-incoming-email.yml b/changelogs/unreleased/use-separate-token-for-incoming-email.yml
new file mode 100644
index 00000000000..e498f8dd0a6
--- /dev/null
+++ b/changelogs/unreleased/use-separate-token-for-incoming-email.yml
@@ -0,0 +1,4 @@
+---
+title: Use separate email-token for incoming email and revert back the inactive feature
+merge_request: 5914
+author:
diff --git a/changelogs/unreleased/user-dropdown-multiple-requests-fix.yml b/changelogs/unreleased/user-dropdown-multiple-requests-fix.yml
new file mode 100644
index 00000000000..a83441b852a
--- /dev/null
+++ b/changelogs/unreleased/user-dropdown-multiple-requests-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed multiple requests sent when opening dropdowns
+merge_request:
+author:
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index f06c4d4ecf2..a8afc36fc78 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -241,6 +241,10 @@ Devise.setup do |config|
end
end
+ if provider['name'] == 'shibboleth'
+ provider['args'][:fail_with_empty_uid] = true
+ end
+
# A Hash from the configuration will be passed as is.
provider_arguments << provider['args'].symbolize_keys
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 82defb0ba71..9cf8465dca8 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -125,6 +125,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
end
resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
+ delete :merged_branches, controller: 'branches', action: :destroy_all_merged
resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
resource :release, only: [:edit, :update]
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 0aec8aedf72..f3531dd30a5 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -34,6 +34,7 @@
- [project_service, 1]
- [clear_database_cache, 1]
- [delete_user, 1]
+ - [delete_merged_branches, 1]
- [expire_build_instance_artifacts, 1]
- [group_destroy, 1]
- [irker, 1]
diff --git a/db/migrate/20161020075734_default_request_access_groups.rb b/db/migrate/20161020075734_default_request_access_groups.rb
new file mode 100644
index 00000000000..9721cc88724
--- /dev/null
+++ b/db/migrate/20161020075734_default_request_access_groups.rb
@@ -0,0 +1,12 @@
+class DefaultRequestAccessGroups < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ def up
+ change_column_default :namespaces, :request_access_enabled, false
+ end
+
+ def down
+ change_column_default :namespaces, :request_access_enabled, true
+ end
+end
diff --git a/db/migrate/20161020075830_default_request_access_projects.rb b/db/migrate/20161020075830_default_request_access_projects.rb
new file mode 100644
index 00000000000..cb790291b24
--- /dev/null
+++ b/db/migrate/20161020075830_default_request_access_projects.rb
@@ -0,0 +1,12 @@
+class DefaultRequestAccessProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ def up
+ change_column_default :projects, :request_access_enabled, false
+ end
+
+ def down
+ change_column_default :projects, :request_access_enabled, true
+ end
+end
diff --git a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
index bea1cfa4c5d..df38591a333 100644
--- a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
+++ b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
@@ -1,7 +1,7 @@
class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
- BATCH_SIZE = 1000
+ BATCH_SIZE = 500
DOWNTIME = false
# This migration is idempotent and there's no sense in throwing away the
@@ -12,34 +12,34 @@ class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration
projects = Arel::Table.new(:projects)
namespaces = Arel::Table.new(:namespaces)
- finder =
+ finder_sql =
projects.
join(namespaces, Arel::Nodes::InnerJoin).
on(projects[:namespace_id].eq(namespaces[:id])).
where(projects[:visibility_level].gt(namespaces[:visibility_level])).
- project(projects[:id]).
- take(BATCH_SIZE)
+ project(projects[:id], namespaces[:visibility_level]).
+ take(BATCH_SIZE).
+ to_sql
- # MySQL requires a derived table to perform this query
- nested_finder =
- projects.
- from(finder.as("AS projects_inner")).
- project(projects[:id])
-
- valuer =
- namespaces.
- where(namespaces[:id].eq(projects[:namespace_id])).
- project(namespaces[:visibility_level])
-
- # Update matching rows until none remain. The finder contains a limit.
+ # Update matching rows in batches. Each batch can cause up to 3 UPDATE
+ # statements, in addition to the SELECT: one per visibility_level
loop do
- updater = Arel::UpdateManager.new(ActiveRecord::Base).
- table(projects).
- set(projects[:visibility_level] => Arel::Nodes::SqlLiteral.new("(#{valuer.to_sql})")).
- where(projects[:id].in(nested_finder))
-
- num_updated = connection.exec_update(updater.to_sql, self.class.name, [])
- break if num_updated == 0
+ to_update = connection.exec_query(finder_sql)
+ break if to_update.rows.count == 0
+
+ # row[0] is projects.id, row[1] is namespaces.visibility_level
+ updates = to_update.rows.each_with_object(Hash.new {|h, k| h[k] = [] }) do |row, obj|
+ obj[row[1]] << row[0]
+ end
+
+ updates.each do |visibility_level, project_ids|
+ updater = Arel::UpdateManager.new(ActiveRecord::Base).
+ table(projects).
+ set(projects[:visibility_level] => visibility_level).
+ where(projects[:id].in(project_ids))
+
+ ActiveRecord::Base.connection.exec_update(updater.to_sql, self.class.name, [])
+ end
end
end
diff --git a/db/schema.rb b/db/schema.rb
index 886be4520a3..ed4dfc786f6 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -723,7 +723,7 @@ ActiveRecord::Schema.define(version: 20161109150329) do
t.string "avatar"
t.boolean "share_with_group_lock", default: false
t.integer "visibility_level", default: 20, null: false
- t.boolean "request_access_enabled", default: true, null: false
+ t.boolean "request_access_enabled", default: false, null: false
t.datetime "deleted_at"
t.boolean "lfs_enabled"
t.text "description_html"
@@ -911,7 +911,7 @@ ActiveRecord::Schema.define(version: 20161109150329) do
t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false
t.boolean "has_external_issue_tracker"
t.string "repository_storage", default: "default", null: false
- t.boolean "request_access_enabled", default: true, null: false
+ t.boolean "request_access_enabled", default: false, null: false
t.boolean "has_external_wiki"
t.boolean "lfs_enabled"
t.text "description_html"
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index fd23047f027..d3f216fb3bf 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -257,6 +257,24 @@ the LDAP server's SSL certificate is performed.
## Troubleshooting
+### Debug LDAP user filter with ldapsearch
+
+This example uses ldapsearch and assumes you are using ActiveDirectory. The
+following query returns the login names of the users that will be allowed to
+log in to GitLab if you configure your own user_filter.
+
+```
+ldapsearch -H ldaps://$host:$port -D "$bind_dn" -y bind_dn_password.txt -b "$base" "$user_filter" sAMAccountName
+```
+
+- Variables beginning with a `$` refer to a variable from the LDAP section of
+ your configuration file.
+- Replace ldaps:// with ldap:// if you are using the plain authentication method.
+ Port `389` is the default `ldap://` port and `636` is the default `ldaps://`
+ port.
+- We are assuming the password for the bind_dn user is in bind_dn_password.txt.
+
+
### Invalid credentials when logging in
- Make sure the user you are binding with has enough permissions to read the user's
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index d74a786ac24..d5a5aef7ec0 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -7,19 +7,10 @@ highly available.
## Architecture
-### Active/Passive
-
-For pure high-availability/failover with no scaling you can use an
-active/passive configuration. This utilizes DRBD (Distributed Replicated
-Block Device) to keep all data in sync. DRBD requires a low latency link to
-remain in sync. It is not advisable to attempt to run DRBD between data centers
-or in different cloud availability zones.
+There are two kinds of setups:
-Components/Servers Required:
-
-- 2 servers/virtual machines (one active/one passive)
-
-![Active/Passive HA Diagram](../img/high_availability/active-passive-diagram.png)
+- active/active
+- active/passive
### Active/Active
@@ -28,12 +19,24 @@ user requests simultaneously. The database, Redis, and GitLab application are
all deployed on separate servers. The configuration is **only** highly-available
if the database, Redis and storage are also configured as such.
-![Active/Active HA Diagram](../img/high_availability/active-active-diagram.png)
-
-**Steps to configure active/active:**
+Follow the steps below to configure an active/active setup:
1. [Configure the database](database.md)
1. [Configure Redis](redis.md)
1. [Configure NFS](nfs.md)
1. [Configure the GitLab application servers](gitlab.md)
1. [Configure the load balancers](load_balancer.md)
+
+![Active/Active HA Diagram](../img/high_availability/active-active-diagram.png)
+
+### Active/Passive
+
+For pure high-availability/failover with no scaling you can use an
+active/passive configuration. This utilizes DRBD (Distributed Replicated
+Block Device) to keep all data in sync. DRBD requires a low latency link to
+remain in sync. It is not advisable to attempt to run DRBD between data centers
+or in different cloud availability zones.
+
+Components/Servers Required: 2 servers/virtual machines (one active/one passive)
+
+![Active/Passive HA Diagram](../img/high_availability/active-passive-diagram.png)
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index 8656b42f274..f9bc4f59345 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -1,265 +1,825 @@
# Configuring Redis for GitLab HA
-You can choose to install and manage Redis yourself, or you can use the one
-that comes bundled with GitLab Omnibus packages.
-
-> **Note:** Redis does not require authentication by default. See
+>
+Experimental Redis Sentinel support was [Introduced][ce-1877] in GitLab 8.11.
+Starting with 8.14, Redis Sentinel is no longer experimental.
+If you've used it with versions `< 8.14` before, please check the updated
+documentation here.
+
+High Availability with [Redis] is possible using a **Master** x **Slave**
+topology with a [Redis Sentinel][sentinel] service to watch and automatically
+start the failover procedure.
+
+You can choose to install and manage Redis and Sentinel yourself, use
+a hosted cloud solution or you can use the one that comes bundled with
+Omnibus GitLab packages.
+
+> **Notes:**
+- Redis requires authentication for High Availability. See
[Redis Security](http://redis.io/topics/security) documentation for more
information. We recommend using a combination of a Redis password and tight
firewall rules to secure your Redis service.
+- You are highly encouraged to read the [Redis Sentinel][sentinel] documentation
+ before configuring Redis HA with GitLab to fully understand the topology and
+ architecture.
+- This is the documentation for the Omnibus GitLab packages. For installations
+ from source, follow the [Redis HA source installation](redis_source.md) guide.
+- Redis Sentinel daemon is bundled with Omnibus GitLab Enterprise Edition only.
+ For configuring Sentinel with the Omnibus GitLab Community Edition and
+ installations from source, read the
+ [Available configuration setups](#available-configuration-setups) section
+ below.
+
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**
+
+- [Overview](#overview)
+ - [High Availability with Sentinel](#high-availability-with-sentinel)
+ - [Recommended setup](#recommended-setup)
+ - [Redis setup overview](#redis-setup-overview)
+ - [Sentinel setup overview](#sentinel-setup-overview)
+ - [Available configuration setups](#available-configuration-setups)
+- [Configuring Redis HA](#configuring-redis-ha)
+ - [Prerequisites](#prerequisites)
+ - [Step 1. Configuring the master Redis instance](#step-1-configuring-the-master-redis-instance)
+ - [Step 2. Configuring the slave Redis instances](#step-2-configuring-the-slave-redis-instances)
+ - [Step 3. Configuring the Redis Sentinel instances](#step-3-configuring-the-redis-sentinel-instances)
+ - [Step 4. Configuring the GitLab application](#step-4-configuring-the-gitlab-application)
+- [Switching from an existing single-machine installation to Redis HA](#switching-from-an-existing-single-machine-installation-to-redis-ha)
+- [Example of a minimal configuration with 1 master, 2 slaves and 3 Sentinels](#example-of-a-minimal-configuration-with-1-master-2-slaves-and-3-sentinels)
+ - [Example configuration for Redis master and Sentinel 1](#example-configuration-for-redis-master-and-sentinel-1)
+ - [Example configuration for Redis slave 1 and Sentinel 2](#example-configuration-for-redis-slave-1-and-sentinel-2)
+ - [Example configuration for Redis slave 2 and Sentinel 3](#example-configuration-for-redis-slave-2-and-sentinel-3)
+ - [Example configuration for the GitLab application](#example-configuration-for-the-gitlab-application)
+- [Advanced configuration](#advanced-configuration)
+ - [Control running services](#control-running-services)
+- [Troubleshooting](#troubleshooting)
+ - [Troubleshooting Redis replication](#troubleshooting-redis-replication)
+ - [Troubleshooting Sentinel](#troubleshooting-sentinel)
+- [Changelog](#changelog)
+- [Further reading](#further-reading)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+## Overview
+
+Before diving into the details of setting up Redis and Redis Sentinel for HA,
+make sure you read this Overview section to better understand how the components
+are tied together.
+
+You need at least `3` independent machines: physical, or VMs running into
+distinct physical machines. It is essential that all master and slaves Redis
+instances run in different machines. If you fail to provision the machines in
+that specific way, any issue with the shared environment can bring your entire
+setup down.
+
+It is OK to run a Sentinel along with a master or slave Redis instance.
+No more than one Sentinel in the same machine though.
+
+You also need to take in consideration the underlying network topology,
+making sure you have redundant connectivity between Redis / Sentinel and
+GitLab instances, otherwise the networks will become a single point of
+failure.
+
+Make sure that you read this document once as a whole before configuring the
+components below.
+
+### High Availability with Sentinel
+
+>**Notes:**
+- Starting with GitLab `8.11`, you can configure a list of Redis Sentinel
+ servers that will monitor a group of Redis servers to provide failover support.
+- Starting with GitLab `8.14`, the Omnibus GitLab Enterprise Edition package
+ comes with Redis Sentinel daemon built-in.
+
+High Availability with Redis requires a few things:
+
+- Multiple Redis instances
+- Run Redis in a **Master** x **Slave** topology
+- Multiple Sentinel instances
+- Application support and visibility to all Sentinel and Redis instances
+
+Redis Sentinel can handle the most important tasks in an HA environment and that's
+to help keep servers online with minimal to no downtime. Redis Sentinel:
+
+- Monitors **Master** and **Slaves** instances to see if they are available
+- Promotes a **Slave** to **Master** when the **Master** fails
+- Demotes a **Master** to **Slave** when the failed **Master** comes back online
+ (to prevent data-partitioning)
+- Can be queried by clients to always connect to the current **Master** server
+
+When a **Master** fails to respond, it's the client's responsibility to handle
+timeout and reconnect (querying a **Sentinel** for a new **Master**).
-## Configure your own Redis server
+To get a better understanding on how to correctly setup Sentinel, please read
+the [Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as
+failing to configure it correctly can lead to data loss or can bring your
+whole cluster down, invalidating the failover effort.
-If you're hosting GitLab on a cloud provider, you can optionally use a
-managed service for Redis. For example, AWS offers a managed ElastiCache service
-that runs Redis.
+### Recommended setup
-## Configure Redis using Omnibus
+For a minimal setup, you will install the Omnibus GitLab package in `3`
+**independent** machines, both with **Redis** and **Sentinel**:
-If you don't want to bother setting up your own Redis server, you can use the
-one bundled with Omnibus. In this case, you should disable all services except
-Redis.
+- Redis Master + Sentinel
+- Redis Slave + Sentinel
+- Redis Slave + Sentinel
-1. Download/install GitLab Omnibus using **steps 1 and 2** from
- [GitLab downloads](https://about.gitlab.com/downloads). Do not complete other
- steps on the download page.
-1. Create/edit `/etc/gitlab/gitlab.rb` and use the following configuration.
- Be sure to change the `external_url` to match your eventual GitLab front-end
- URL:
+If you are not sure or don't understand why and where the amount of nodes come
+from, read [Redis setup overview](#redis-setup-overview) and
+[Sentinel setup overview](#sentinel-setup-overview).
- ```ruby
- external_url 'https://gitlab.example.com'
-
- # Disable all services except Redis
- redis['enable'] = true
- bootstrap['enable'] = false
- nginx['enable'] = false
- unicorn['enable'] = false
- sidekiq['enable'] = false
- postgresql['enable'] = false
- gitlab_workhorse['enable'] = false
- mailroom['enable'] = false
-
- # Redis configuration
- redis['port'] = 6379
- redis['bind'] = '0.0.0.0'
+For a recommended setup that can resist more failures, you will install
+the Omnibus GitLab package in `5` **independent** machines, both with
+**Redis** and **Sentinel**:
- # If you wish to use Redis authentication (recommended)
- redis['password'] = 'Redis Password'
- ```
+- Redis Master + Sentinel
+- Redis Slave + Sentinel
+- Redis Slave + Sentinel
+- Redis Slave + Sentinel
+- Redis Slave + Sentinel
-1. Run `sudo touch /etc/gitlab/skip-auto-migrations` to prevent database migrations
- from running on upgrade. Only the primary GitLab application server should
- handle migrations.
+### Redis setup overview
-1. Run `sudo gitlab-ctl reconfigure` to install and configure Redis.
+You must have at least `3` Redis servers: `1` Master, `2` Slaves, and they
+need to be each in a independent machine (see explanation above).
- > **Note**: This `reconfigure` step will result in some errors.
- That's OK - don't be alarmed.
+You can have additional Redis nodes, that will help survive a situation
+where more nodes goes down. Whenever there is only `2` nodes online, a failover
+will not be initiated.
-## Experimental Redis Sentinel support
+As an example, if you have `6` Redis nodes, a maximum of `3` can be
+simultaneously down.
-> [Introduced][ce-1877] in GitLab 8.11.
+Please note that there are different requirements for Sentinel nodes.
+If you host them in the same Redis machines, you may need to take
+that restrictions into consideration when calculating the amount of
+nodes to be provisioned. See [Sentinel setup overview](#sentinel-setup-overview)
+documentation for more information.
-Since GitLab 8.11, you can configure a list of Redis Sentinel servers that
-will monitor a group of Redis servers to provide you with a standard failover
-support.
+All Redis nodes should be configured the same way and with similar server specs, as
+in a failover situation, any **Slave** can be promoted as the new **Master** by
+the Sentinel servers.
-There is currently one exception to the Sentinel support: `mail_room`, the
-component that processes incoming emails. It doesn't support Sentinel yet, but
-we hope to integrate a future release that does support it.
+The replication requires authentication, so you need to define a password to
+protect all Redis nodes and the Sentinels. They will all share the same
+password, and all instances must be able to talk to
+each other over the network.
-To get a better understanding on how to correctly setup Sentinel, please read
-the [Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as
-failing to configure it correctly can lead to data loss.
+### Sentinel setup overview
-The configuration consists of three parts:
+Sentinels watch both other Sentinels and Redis nodes. Whenever a Sentinel
+detects that a Redis node is not responding, it will announce that to the
+other Sentinels. They have to reach the **quorum**, that is the minimum amount
+of Sentinels that agrees a node is down, in order to be able to start a failover.
-- Redis setup
-- Sentinel setup
-- GitLab setup
+Whenever the **quorum** is met, the **majority** of all known Sentinel nodes
+need to be available and reachable, so that they can elect the Sentinel **leader**
+who will take all the decisions to restore the service availability by:
-Read carefully how to configure those components below.
+- Promoting a new **Master**
+- Reconfiguring the other **Slaves** and make them point to the new **Master**
+- Announce the new **Master** to every other Sentinel peer
+- Reconfigure the old **Master** and demote to **Slave** when it comes back online
-### Redis setup
+You must have at least `3` Redis Sentinel servers, and they need to
+be each in a independent machine (that are believed to fail independently),
+ideally in different geographical areas.
-You must have at least 2 Redis servers: 1 Master, 1 or more Slaves.
-They should be configured the same way and with similar server specs, as
-in a failover situation, any Slave can be elected as the new Master by
-the Sentinel servers.
+You can configure them in the same machines where you've configured the other
+Redis servers, but understand that if a whole node goes down, you loose both
+a Sentinel and a Redis instance.
-In a minimal setup, the only required change for the slaves in `redis.conf`
-is the addition of a `slaveof` line pointing to the initial master.
-You can increase the security by defining a `requirepass` configuration in
-the master, and `masterauth` in slaves.
+The number of sentinels should ideally always be an **odd** number, for the
+consensus algorithm to be effective in the case of a failure.
----
+In a `3` nodes topology, you can only afford `1` Sentinel node going down.
+Whenever the **majority** of the Sentinels goes down, the network partition
+protection prevents destructive actions and a failover **will not be started**.
-**Configuring your own Redis server**
+Here are some examples:
-1. Add to the slaves' `redis.conf`:
+- With `5` or `6` sentinels, a maximum of `2` can go down for a failover begin.
+- With `7` sentinels, a maximum of `3` nodes can go down.
- ```conf
- # IP and port of the master Redis server
- slaveof 10.10.10.10 6379
- ```
+The **Leader** election can sometimes fail the voting round when **consensus**
+is not achieved (see the odd number of nodes requirement above). In that case,
+a new attempt will be made after the amount of time defined in
+`sentinel['failover_timeout']` (in milliseconds).
-1. Optionally, set up password authentication for increased security.
- Add the following to master's `redis.conf`:
+>**Note:**
+We will see where `sentinel['failover_timeout']` is defined later.
+
+The `failover_timeout` variable has a lot of different use cases. According to
+the official documentation:
+
+- The time needed to re-start a failover after a previous failover was
+ already tried against the same master by a given Sentinel, is two
+ times the failover timeout.
+
+- The time needed for a slave replicating to a wrong master according
+ to a Sentinel current configuration, to be forced to replicate
+ with the right master, is exactly the failover timeout (counting since
+ the moment a Sentinel detected the misconfiguration).
+
+- The time needed to cancel a failover that is already in progress but
+ did not produced any configuration change (SLAVEOF NO ONE yet not
+ acknowledged by the promoted slave).
+
+- The maximum time a failover in progress waits for all the slaves to be
+ reconfigured as slaves of the new master. However even after this time
+ the slaves will be reconfigured by the Sentinels anyway, but not with
+ the exact parallel-syncs progression as specified.
+
+### Available configuration setups
+
+Based on your infrastructure setup and how you have installed GitLab, there are
+multiple ways to configure Redis HA. Omnibus GitLab packages have Redis and/or
+Redis Sentinel bundled with them so you only need to focus on configuration.
+Pick the one that suits your needs.
+
+- [Installations from source][source]: You need to install Redis and Sentinel
+ yourself. Use the [Redis HA installation from source](redis_source.md)
+ documentation.
+- [Omnibus GitLab **Community Edition** (CE) package][ce]: Redis is bundled, so you
+ can use the package with only the Redis service enabled as described in steps
+ 1 and 2 of this document (works for both master and slave setups). To install
+ and configure Sentinel, jump directly to the Sentinel section in the
+ [Redis HA installation from source](redis_source.md#step-3-configuring-the-redis-sentinel-instances) documentation.
+- [Omnibus GitLab **Enterprise Edition** (EE) package][ee]: Both Redis and Sentinel
+ are bundled in the package, so you can use the EE package to setup the whole
+ Redis HA infrastructure (master, slave and Sentinel) which is described in
+ this document.
+- If you have installed GitLab using the Omnibus GitLab packages (CE or EE),
+ but you want to use your own external Redis server, follow steps 1-3 in the
+ [Redis HA installation from source](redis_source.md) documentation, then go
+ straight to step 4 in this guide to
+ [set up the GitLab application](#step-4-configuring-the-gitlab-application).
+
+## Configuring Redis HA
+
+This is the section where we install and setup the new Redis instances.
+
+>**Notes:**
+- We assume that you install GitLab and all HA components from scratch. If you
+ already have it installed and running, read how to
+ [switch from a single-machine installation to Redis HA](#switching-from-an-existing-single-machine-installation-to-redis-ha).
+- Redis nodes (both master and slaves) will need the same password defined in
+ `redis['password']`. At any time during a failover the Sentinels can
+ reconfigure a node and change its status from master to slave and vice versa.
+
+### Prerequisites
+
+The prerequisites for a HA Redis setup are the following:
+
+1. Provision the minimum required number of instances as specified in the
+ [recommended setup](#recommended-setup) section.
+1. **Do NOT** install Redis or Redis Sentinel in the same machines your
+ GitLab application is running on. You can however opt in to install Redis
+ and Sentinel in the same machine (each in independent ones is recommended
+ though).
+1. All Redis nodes must be able to talk to each other and accept incoming
+ connections over Redis (`6379`) and Sentinel (`26379`) ports (unless you
+ change the default ones).
+1. The server that hosts the GitLab application must be able to access the
+ Redis nodes.
+1. Protect the nodes from access from external networks ([Internet][it]), using
+ firewall.
+
+### Step 1. Configuring the master Redis instance
+
+1. SSH into the **master** Redis server and login as root:
- ```conf
- # Optional password authentication for increased security
- requirepass "<password>"
```
+ sudo -i
+ ```
+
+1. [Download/install](https://about.gitlab.com/installation) the Omnibus GitLab
+ package you want using **steps 1 and 2** from the GitLab downloads page.
+ - Make sure you select the correct Omnibus package, with the same version
+ and type (Community, Enterprise editions) of your current install.
+ - Do not complete any other steps on the download page.
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
-1. Then add this line to all the slave servers' `redis.conf`:
+ ```ruby
+ # Enable the master role and disable all other services in the machine
+ # (you can still enable Sentinel).
+ redis_master_role['enable'] = true
+
+ # IP address pointing to a local IP that the other machines can reach to.
+ # You can also set bind to '0.0.0.0' which listen in all interfaces.
+ # If you really need to bind to an external accessible IP, make
+ # sure you add extra firewall rules to prevent unauthorized access.
+ redis['bind'] = '10.0.0.1'
+
+ # Define a port so Redis can listen for TCP requests which will allow other
+ # machines to connect to it.
+ redis['port'] = 6379
- ```conf
- masterauth "<password>"
+ # Set up password authentication for Redis (use the same password in all nodes).
+ redis['password'] = 'redis-password-goes-here'
```
-1. Restart the Redis services for the changes to take effect.
+1. To prevent database migrations from running on upgrade, run:
----
+ ```
+ sudo touch /etc/gitlab/skip-auto-migrations
+ ```
-**Using Redis via Omnibus**
+ Only the primary GitLab application server should handle migrations.
-1. Edit `/etc/gitlab/gitlab.rb` of a master Redis machine (usualy a single machine):
+1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
- ```ruby
- ## Redis TCP support (will disable UNIX socket transport)
- redis['bind'] = '0.0.0.0' # or specify an IP to bind to a single one
- redis['port'] = 6379
+### Step 2. Configuring the slave Redis instances
+
+1. SSH into the **slave** Redis server and login as root:
- ## Master redis instance
- redis['password'] = '<huge password string here>'
```
+ sudo -i
+ ```
+
+1. [Download/install](https://about.gitlab.com/installation) the Omnibus GitLab
+ package you want using **steps 1 and 2** from the GitLab downloads page.
+ - Make sure you select the correct Omnibus package, with the same version
+ and type (Community, Enterprise editions) of your current install.
+ - Do not complete any other steps on the download page.
-1. Edit `/etc/gitlab/gitlab.rb` of a slave Redis machine (should be one or more machines):
+1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
```ruby
- ## Redis TCP support (will disable UNIX socket transport)
- redis['bind'] = '0.0.0.0' # or specify an IP to bind to a single one
+ # Enable the slave role and disable all other services in the machine
+ # (you can still enable Sentinel). This will also set automatically
+ # `redis['master'] = false`.
+ redis_slave_role['enable'] = true
+
+ # IP address pointing to a local IP that the other machines can reach to.
+ # You can also set bind to '0.0.0.0' which listen in all interfaces.
+ # If you really need to bind to an external accessible IP, make
+ # sure you add extra firewall rules to prevent unauthorized access.
+ redis['bind'] = '10.0.0.2'
+
+ # Define a port so Redis can listen for TCP requests which will allow other
+ # machines to connect to it.
redis['port'] = 6379
- ## Slave redis instance
- redis['master_ip'] = '10.10.10.10' # IP of master Redis server
- redis['master_port'] = 6379 # Port of master Redis server
- redis['master_password'] = "<huge password string here>"
+ # The same password for Redeis authentication you set up for the master node.
+ redis['password'] = 'redis-password-goes-here'
+
+ # The IP of the master Redis node.
+ redis['master_ip'] = '10.0.0.1'
+
+ # Port of master Redis server, uncomment to change to non default. Defaults
+ # to `6379`.
+ #redis['master_port'] = 6379
+ ```
+
+1. To prevent database migrations from running on upgrade, run:
+
+ ```
+ sudo touch /etc/gitlab/skip-auto-migrations
```
-1. Reconfigure the GitLab for the changes to take effect: `sudo gitlab-ctl reconfigure`
+ Only the primary GitLab application server should handle migrations.
+
+1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+1. Go through the steps again for all the other slave nodes.
---
+These values don't have to be changed again in `/etc/gitlab/gitlab.rb` after
+a failover, as the nodes will be managed by the Sentinels, and even after a
+`gitlab-ctl reconfigure`, they will get their configuration restored by
+the same Sentinels.
+
+### Step 3. Configuring the Redis Sentinel instances
+
+>**Note:**
+Redis Sentinel is bundled with Omnibus GitLab Enterprise Edition only. The
+following section assumes you are using Omnibus GitLab Enterprise Edition.
+For the Omnibus Community Edition and installations from source, follow the
+[Redis HA source install](redis_source.md) guide.
+
Now that the Redis servers are all set up, let's configure the Sentinel
servers.
-### Sentinel setup
+If you are not sure if your Redis servers are working and replicating
+correctly, please read the [Troubleshooting Replication](#troubleshooting-replication)
+and fix it before proceeding with Sentinel setup.
-We don't provide yet an automated way to setup and run the Sentinel daemon
-from Omnibus installation method. You must follow the instructions below and
-run it by yourself.
+You must have at least `3` Redis Sentinel servers, and they need to
+be each in an independent machine. You can configure them in the same
+machines where you've configured the other Redis servers.
-The support for Sentinel in Ruby has some [caveats](https://github.com/redis/redis-rb/issues/531).
-While you can give any name for the `master-group-name` part of the
-configuration, as in this example:
+With GitLab Enterprise Edition, you can use the Omnibus package to setup
+multiple machines with the Sentinel daemon.
-```conf
-sentinel monitor <master-group-name> <ip> <port> <quorum>
-```
+---
+
+1. SSH into the server that will host Redis Sentinel and login as root:
-,for it to work in Ruby, you have to use the "hostname" of the master Redis
-server, otherwise you will get an error message like:
-`Redis::CannotConnectError: No sentinels available.`. Read
-[Sentinel troubleshooting](#sentinel-troubleshooting) for more information.
+ ```
+ sudo -i
+ ```
-Here is an example configuration file (`sentinel.conf`) for a Sentinel node:
+1. **You can omit this step if the Sentinels will be hosted in the same node as
+ the other Redis instances.**
-```conf
-port 26379
-sentinel monitor master-redis.example.com 10.10.10.10 6379 1
-sentinel down-after-milliseconds master-redis.example.com 10000
-sentinel config-epoch master-redis.example.com 0
-sentinel leader-epoch master-redis.example.com 0
-```
+ [Download/install](https://about.gitlab.com/downloads-ee) the
+ Omnibus GitLab Enterprise Edition package using **steps 1 and 2** from the
+ GitLab downloads page.
+ - Make sure you select the correct Omnibus package, with the same version
+ the GitLab application is running.
+ - Do not complete any other steps on the download page.
----
+1. Edit `/etc/gitlab/gitlab.rb` and add the contents (if you are installing the
+ Sentinels in the same node as the other Redis instances, some values might
+ be duplicate below):
-The final part is to inform the main GitLab application server of the Redis
-master and the new sentinels servers.
-### GitLab setup
+ ```ruby
+ redis_sentinel_role['enable'] = true
-You can enable or disable sentinel support at any time in new or existing
-installations. From the GitLab application perspective, all it requires is
-the correct credentials for the master Redis and for a few Sentinel nodes.
+ # Must be the same in every sentinel node
+ redis['master_name'] = 'gitlab-redis'
-It doesn't require a list of all Sentinel nodes, as in case of a failure,
-the application will need to query only one of them.
+ # The same password for Redis authentication you set up for the master node.
+ redis['password'] = 'redis-password-goes-here'
->**Note:**
-The following steps should be performed in the [GitLab application server](gitlab.md).
+ # The IP of the master Redis node.
+ redis['master_ip'] = '10.0.0.1'
-**For source based installations**
+ # Define a port so Redis can listen for TCP requests which will allow other
+ # machines to connect to it.
+ redis['port'] = 6379
+
+ # Port of master Redis server, uncomment to change to non default. Defaults
+ # to `6379`.
+ #redis['master_port'] = 6379
+
+ ## Configure Sentinel
+ sentinel['bind'] = '10.0.0.1'
+
+ # Port that Sentinel listens on, uncomment to change to non default. Defaults
+ # to `26379`.
+ # sentinel['port'] = 26379
+
+ ## Quorum must reflect the amount of voting sentinels it take to start a failover.
+ ## Value must NOT be greater then the amount of sentinels.
+ ##
+ ## The quorum can be used to tune Sentinel in two ways:
+ ## 1. If a the quorum is set to a value smaller than the majority of Sentinels
+ ## we deploy, we are basically making Sentinel more sensible to master failures,
+ ## triggering a failover as soon as even just a minority of Sentinels is no longer
+ ## able to talk with the master.
+ ## 1. If a quorum is set to a value greater than the majority of Sentinels, we are
+ ## making Sentinel able to failover only when there are a very large number (larger
+ ## than majority) of well connected Sentinels which agree about the master being down.s
+ sentinel['quorum'] = 2
+
+ ## Consider unresponsive server down after x amount of ms.
+ # sentinel['down_after_milliseconds'] = 10000
+
+ ## Specifies the failover timeout in milliseconds. It is used in many ways:
+ ##
+ ## - The time needed to re-start a failover after a previous failover was
+ ## already tried against the same master by a given Sentinel, is two
+ ## times the failover timeout.
+ ##
+ ## - The time needed for a slave replicating to a wrong master according
+ ## to a Sentinel current configuration, to be forced to replicate
+ ## with the right master, is exactly the failover timeout (counting since
+ ## the moment a Sentinel detected the misconfiguration).
+ ##
+ ## - The time needed to cancel a failover that is already in progress but
+ ## did not produced any configuration change (SLAVEOF NO ONE yet not
+ ## acknowledged by the promoted slave).
+ ##
+ ## - The maximum time a failover in progress waits for all the slaves to be
+ ## reconfigured as slaves of the new master. However even after this time
+ ## the slaves will be reconfigured by the Sentinels anyway, but not with
+ ## the exact parallel-syncs progression as specified.
+ # sentinel['failover_timeout'] = 60000
+ ```
+
+1. To prevent database migrations from running on upgrade, run:
+
+ ```
+ sudo touch /etc/gitlab/skip-auto-migrations
+ ```
+
+ Only the primary GitLab application server should handle migrations.
-1. Edit `/home/git/gitlab/config/resque.yml` following the example in
- `/home/git/gitlab/config/resque.yml.example`, and uncomment the sentinels
- line, changing to the correct server credentials.
-1. Restart GitLab for the changes to take effect.
+1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+1. Go through the steps again for all the other Sentinel nodes.
-**For Omnibus installations**
+### Step 4. Configuring the GitLab application
+
+The final part is to inform the main GitLab application server of the Redis
+Sentinels servers and authentication credentials.
+
+You can enable or disable Sentinel support at any time in new or existing
+installations. From the GitLab application perspective, all it requires is
+the correct credentials for the Sentinel nodes.
+
+While it doesn't require a list of all Sentinel nodes, in case of a failure,
+it needs to access at least one of the listed.
+
+>**Note:**
+The following steps should be performed in the [GitLab application server](gitlab.md)
+which ideally should not have Redis or Sentinels on it for a HA setup.
1. Edit `/etc/gitlab/gitlab.rb` and add/change the following lines:
- ```ruby
- gitlab-rails['redis_host'] = "master-redis.example.com"
- gitlab-rails['redis_port'] = 6379
- gitlab-rails['redis_password'] = '<huge password string here>'
- gitlab-rails['redis_sentinels'] = [
- {'host' => '10.10.10.1', 'port' => 26379},
- {'host' => '10.10.10.2', 'port' => 26379},
- {'host' => '10.10.10.3', 'port' => 26379}
+ ```
+ ## Must be the same in every sentinel node
+ redis['master_name'] = 'gitlab-redis'
+
+ ## The same password for Redis authentication you set up for the master node.
+ redis['password'] = 'redis-password-goes-here'
+
+ ## A list of sentinels with `host` and `port`
+ gitlab_rails['redis_sentinels'] = [
+ {'host' => '10.0.0.1', 'port' => 26379},
+ {'host' => '10.0.0.2', 'port' => 26379},
+ {'host' => '10.0.0.3', 'port' => 26379}
]
```
-1. [Reconfigure] the GitLab for the changes to take effect.
+1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
-### Sentinel troubleshooting
+## Switching from an existing single-machine installation to Redis HA
-If you get an error like: `Redis::CannotConnectError: No sentinels available.`,
-there may be something wrong with your configuration files or it can be related
-to [this issue][gh-531] ([pull request][gh-534] that should make things better).
+If you already have a single-machine GitLab install running, you will need to
+replicate from this machine first, before de-activating the Redis instance
+inside it.
+
+Your single-machine install will be the initial **Master**, and the `3` others
+should be configured as **Slave** pointing to this machine.
+
+After replication catches up, you will need to stop services in the
+single-machine install, to rotate the **Master** to one of the new nodes.
+
+Make the required changes in configuration and restart the new nodes again.
+
+To disable redis in the single install, edit `/etc/gitlab/gitlab.rb`:
+
+```ruby
+redis['enable'] = false
+```
+
+If you fail to replicate first, you may loose data (unprocessed background jobs).
+
+## Example of a minimal configuration with 1 master, 2 slaves and 3 Sentinels
+
+>**Note:**
+Redis Sentinel is bundled with Omnibus GitLab Enterprise Edition only. For
+different setups, read the
+[available configuration setups](#available-configuration-setups) section.
+
+In this example we consider that all servers have an internal network
+interface with IPs in the `10.0.0.x` range, and that they can connect
+to each other using these IPs.
+
+In a real world usage, you would also setup firewall rules to prevent
+unauthorized access from other machines and block traffic from the
+outside (Internet).
+
+We will use the same `3` nodes with **Redis** + **Sentinel** topology
+discussed in [Redis setup overview](#redis-setup-overview) and
+[Sentinel setup overview](#sentinel-setup-overview) documentation.
+
+Here is a list and description of each **machine** and the assigned **IP**:
+
+* `10.0.0.1`: Redis Master + Sentinel 1
+* `10.0.0.2`: Redis Slave 1 + Sentinel 2
+* `10.0.0.3`: Redis Slave 2 + Sentinel 3
+* `10.0.0.4`: GitLab application
+
+Please note that after the initial configuration, if a failover is initiated
+by the Sentinel nodes, the Redis nodes will be reconfigured and the **Master**
+will change permanently (including in `redis.conf`) from one node to the other,
+until a new failover is initiated again.
+
+The same thing will happen with `sentinel.conf` that will be overridden after the
+initial execution, after any new sentinel node starts watching the **Master**,
+or a failover promotes a different **Master** node.
+
+### Example configuration for Redis master and Sentinel 1
+
+In `/etc/gitlab/gitlab.rb`:
+
+```ruby
+redis_master_role['enable'] = true
+redis_sentinel_role['enable'] = true
+redis['bind'] = '10.0.0.1'
+redis['port'] = 6379
+redis['password'] = 'redis-password-goes-here'
+redis['master_name'] = 'gitlab-redis' # must be the same in every sentinel node
+redis['master_password'] = 'redis-password-goes-here' # the same value defined in redis['password'] in the master instance
+redis['master_ip'] = '10.0.0.1' # ip of the initial master redis instance
+#redis['master_port'] = 6379 # port of the initial master redis instance, uncomment to change to non default
+sentinel['bind'] = '10.0.0.1'
+# sentinel['port'] = 26379 # uncomment to change default port
+sentinel['quorum'] = 2
+# sentinel['down_after_milliseconds'] = 10000
+# sentinel['failover_timeout'] = 60000
+```
+
+[Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+
+### Example configuration for Redis slave 1 and Sentinel 2
+
+In `/etc/gitlab/gitlab.rb`:
+
+```ruby
+redis_slave_role['enable'] = true
+redis_sentinel_role['enable'] = true
+redis['bind'] = '10.0.0.2'
+redis['port'] = 6379
+redis['password'] = 'redis-password-goes-here'
+redis['master_password'] = 'redis-password-goes-here'
+redis['master_ip'] = '10.0.0.1' # IP of master Redis server
+#redis['master_port'] = 6379 # Port of master Redis server, uncomment to change to non default
+redis['master_name'] = 'gitlab-redis' # must be the same in every sentinel node
+sentinel['bind'] = '10.0.0.2'
+# sentinel['port'] = 26379 # uncomment to change default port
+sentinel['quorum'] = 2
+# sentinel['down_after_milliseconds'] = 10000
+# sentinel['failover_timeout'] = 60000
+```
+
+[Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+
+### Example configuration for Redis slave 2 and Sentinel 3
+
+In `/etc/gitlab/gitlab.rb`:
+
+```ruby
+redis_slave_role['enable'] = true
+redis_sentinel_role['enable'] = true
+redis['bind'] = '10.0.0.3'
+redis['port'] = 6379
+redis['password'] = 'redis-password-goes-here'
+redis['master_password'] = 'redis-password-goes-here'
+redis['master_ip'] = '10.0.0.1' # IP of master Redis server
+#redis['master_port'] = 6379 # Port of master Redis server, uncomment to change to non default
+redis['master_name'] = 'gitlab-redis' # must be the same in every sentinel node
+sentinel['bind'] = '10.0.0.3'
+# sentinel['port'] = 26379 # uncomment to change default port
+sentinel['quorum'] = 2
+# sentinel['down_after_milliseconds'] = 10000
+# sentinel['failover_timeout'] = 60000
+```
+
+[Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+
+### Example configuration for the GitLab application
+
+In `/etc/gitlab/gitlab.rb`:
+
+```ruby
+redis['master_name'] = 'gitlab-redis'
+redis['password'] = 'redis-password-goes-here'
+gitlab_rails['redis_sentinels'] = [
+ {'host' => '10.0.0.1', 'port' => 26379},
+ {'host' => '10.0.0.2', 'port' => 26379},
+ {'host' => '10.0.0.3', 'port' => 26379}
+]
+```
+
+[Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+
+## Advanced configuration
+
+Omnibus GitLab configures some things behind the curtains to make the sysadmins'
+lives easier. If you want to know what happens underneath keep reading.
+
+### Control running services
+
+In the previous example, we've used `redis_sentinel_role` and
+`redis_master_role` which simplifies the amount of configuration changes.
+
+If you want more control, here is what each one sets for you automatically
+when enabled:
+
+```ruby
+## Redis Sentinel Role
+redis_sentinel_role['enable'] = true
-It's a bit rigid the way you have to config `resque.yml` and `sentinel.conf`,
-otherwise `redis-rb` will not work properly.
+# When Sentinel Role is enabled, the following services are also enabled
+sentinel['enable'] = true
-The hostname ('my-primary-redis') of the primary Redis server (`sentinel.conf`)
-**must** match the one configured in GitLab (`resque.yml` for source installations
-or `gitlab-rails['redis_*']` in Omnibus) and it must be valid ex:
+# The following services are disabled
+redis['enable'] = false
+bootstrap['enable'] = false
+nginx['enable'] = false
+postgresql['enable'] = false
+gitlab_rails['enable'] = false
+mailroom['enable'] = false
+
+-------
+
+## Redis master/slave Role
+redis_master_role['enable'] = true # enable only one of them
+redis_slave_role['enable'] = true # enable only one of them
+
+# When Redis Master or Slave role are enabled, the following services are
+# enabled/disabled. Note that if Redis and Sentinel roles are combined, both
+# services will be enabled.
+
+# The following services are disabled
+sentinel['enable'] = false
+bootstrap['enable'] = false
+nginx['enable'] = false
+postgresql['enable'] = false
+gitlab_rails['enable'] = false
+mailroom['enable'] = false
+
+# For Redis Slave role, also change this setting from default 'true' to 'false':
+redis['master'] = false
+```
+
+You can find the relevant attributes defined in [gitlab_rails.rb][omnifile].
+
+## Troubleshooting
+
+There are a lot of moving parts that needs to be taken care carefully
+in order for the HA setup to work as expected.
+
+Before proceeding with the troubleshooting below, check your firewall rules:
+
+- Redis machines
+ - Accept TCP connection in `6379`
+ - Connect to the other Redis machines via TCP in `6379`
+- Sentinel machines
+ - Accept TCP connection in `26379`
+ - Connect to other Sentinel machines via TCP in `26379`
+ - Connect to the Redis machines via TCP in `6379`
+
+### Troubleshooting Redis replication
+
+You can check if everything is correct by connecting to each server using
+`redis-cli` application, and sending the `INFO` command.
+
+If authentication was correctly defined, it should fail with:
+`NOAUTH Authentication required` error. Try to authenticate with the
+previous defined password with `AUTH redis-password-goes-here` and
+try the `INFO` command again.
+
+Look for the `# Replication` section where you should see some important
+information like the `role` of the server.
+
+When connected to a `master` redis, you will see the number of connected
+`slaves`, and a list of each with connection details:
-```conf
-# sentinel.conf:
-sentinel monitor my-primary-redis 10.10.10.10 6379 1
-sentinel down-after-milliseconds my-primary-redis 10000
-sentinel config-epoch my-primary-redis 0
-sentinel leader-epoch my-primary-redis 0
```
+# Replication
+role:master
+connected_slaves:1
+slave0:ip=10.133.5.21,port=6379,state=online,offset=208037514,lag=1
+master_repl_offset:208037658
+repl_backlog_active:1
+repl_backlog_size:1048576
+repl_backlog_first_byte_offset:206989083
+repl_backlog_histlen:1048576
+```
+
+When it's a `slave`, you will see details of the master connection and if
+its `up` or `down`:
-```yaml
-# resque.yaml
-production:
- url: redis://my-primary-redis:6378
- sentinels:
- -
- host: slave1
- port: 26380 # point to sentinel, not to redis port
- -
- host: slave2
- port: 26381 # point to sentinel, not to redis port
```
+# Replication
+role:slave
+master_host:10.133.1.58
+master_port:6379
+master_link_status:up
+master_last_io_seconds_ago:1
+master_sync_in_progress:0
+slave_repl_offset:208096498
+slave_priority:100
+slave_read_only:1
+connected_slaves:0
+master_repl_offset:0
+repl_backlog_active:0
+repl_backlog_size:1048576
+repl_backlog_first_byte_offset:0
+repl_backlog_histlen:0
+```
+
+### Troubleshooting Sentinel
+
+If you get an error like: `Redis::CannotConnectError: No sentinels available.`,
+there may be something wrong with your configuration files or it can be related
+to [this issue][gh-531].
+
+You must make sure you are defining the same value in `redis['master_name']`
+and `redis['master_pasword']` as you defined for your sentinel node.
-When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics/sentinel)
+The way the redis connector `redis-rb` works with sentinel is a bit
+non-intuitive. We try to hide the complexity in omnibus, but it still requires
+a few extra configs.
---
@@ -273,7 +833,7 @@ To make sure your configuration is correct:
sudo gitlab-rails console
# For source installations
- sudo -u git rails console RAILS_ENV=production
+ sudo -u git rails console production
```
1. Run in the console:
@@ -288,8 +848,8 @@ To make sure your configuration is correct:
1. To simulate a failover on master Redis, SSH into the Redis server and run:
```bash
- # port must match your master redis port
- redis-cli -h localhost -p 6379 DEBUG sleep 60
+ # port must match your master redis port, and the sleep time must be a few seconds bigger than defined one
+ redis-cli -h localhost -p 6379 DEBUG sleep 20
```
1. Then back in the Rails console from the first step, run:
@@ -301,10 +861,26 @@ To make sure your configuration is correct:
You should see a different port after a few seconds delay
(the failover/reconnect time).
----
-Read more on high-availability configuration:
+## Changelog
+
+Changes to Redis HA over time.
+
+**8.14**
+
+- Redis Sentinel support is production-ready and bundled in the Omnibus GitLab
+ Enterprise Edition package
+- Documentation restructure for better readability
+
+**8.11**
+
+- Experimental Redis Sentinel support was added
+
+## Further reading
+
+Read more on High Availability:
+1. [High Availability Overview](README.md)
1. [Configure the database](database.md)
1. [Configure NFS](nfs.md)
1. [Configure the GitLab application servers](gitlab.md)
@@ -315,3 +891,10 @@ Read more on high-availability configuration:
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[gh-531]: https://github.com/redis/redis-rb/issues/531
[gh-534]: https://github.com/redis/redis-rb/issues/534
+[redis]: http://redis.io/
+[sentinel]: http://redis.io/topics/sentinel
+[omnifile]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/libraries/gitlab_rails.rb
+[source]: ../../install/installation.md
+[ce]: https://about.gitlab.com/downloads
+[ee]: https://about.gitlab.com/downloads-ee
+[it]: https://gitlab.com/gitlab-org/gitlab-ce/uploads/c4cc8cd353604bd80315f9384035ff9e/The_Internet_IT_Crowd.png
diff --git a/doc/administration/high_availability/redis_source.md b/doc/administration/high_availability/redis_source.md
new file mode 100644
index 00000000000..8558ba82d63
--- /dev/null
+++ b/doc/administration/high_availability/redis_source.md
@@ -0,0 +1,387 @@
+# Configuring non-Omnibus Redis for GitLab HA
+
+This is the documentation for configuring a Highly Available Redis setup when
+you have installed Redis all by yourself and not using the bundled one that
+comes with the Omnibus packages.
+
+We cannot stress enough the importance of reading the
+[Overview section](redis.md#overview) of the Omnibus Redis HA as it provides
+some invaluable information to the configuration of Redis. Please proceed to
+read it before going forward with this guide.
+
+We also highly recommend that you use the Omnibus GitLab packages, as we
+optimize them specifically for GitLab, and we will take care of upgrading Redis
+to the latest supported version.
+
+If you're not sure whether this guide is for you, please refer to
+[Available configuration setups](redis.md#available-configuration-setups) in
+the Omnibus Redis HA documentation.
+
+---
+
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**
+
+- [Configuring your own Redis server](#configuring-your-own-redis-server)
+ - [Prerequisites](#prerequisites)
+ - [Step 1. Configuring the master Redis instance](#step-1-configuring-the-master-redis-instance)
+ - [Step 2. Configuring the slave Redis instances](#step-2-configuring-the-slave-redis-instances)
+ - [Step 3. Configuring the Redis Sentinel instances](#step-3-configuring-the-redis-sentinel-instances)
+ - [Step 4. Configuring the GitLab application](#step-4-configuring-the-gitlab-application)
+- [Example of minimal configuration with 1 master, 2 slaves and 3 Sentinels](#example-of-minimal-configuration-with-1-master-2-slaves-and-3-sentinels)
+ - [Example configuration for Redis master and Sentinel 1](#example-configuration-for-redis-master-and-sentinel-1)
+ - [Example configuration for Redis slave 1 and Sentinel 2](#example-configuration-for-redis-slave-1-and-sentinel-2)
+ - [Example configuration for Redis slave 2 and Sentinel 3](#example-configuration-for-redis-slave-2-and-sentinel-3)
+ - [Example configuration of the GitLab application](#example-configuration-of-the-gitlab-application)
+- [Troubleshooting](#troubleshooting)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+## Configuring your own Redis server
+
+This is the section where we install and setup the new Redis instances.
+
+### Prerequisites
+
+- All Redis servers in this guide must be configured to use a TCP connection
+ instead of a socket. To configure Redis to use TCP connections you need to
+ define both `bind` and `port` in the Redis config file. You can bind to all
+ interfaces (`0.0.0.0`) or specify the IP of the desired interface
+ (e.g., one from an internal network).
+- Since Redis 3.2, you must define a password to receive external connections
+ (`requirepass`).
+- If you are using Redis with Sentinel, you will also need to define the same
+ password for the slave password definition (`masterauth`) in the same instance.
+
+In addition, read the prerequisites as described in the
+[Omnibus Redis HA document](redis.md#prerequisites) since they provide some
+valuable information for the general setup.
+
+### Step 1. Configuring the master Redis instance
+
+Assuming that the Redis master instance IP is `10.0.0.1`:
+
+1. [Install Redis](../../install/installation.md#6-redis)
+1. Edit `/etc/redis/redis.conf`:
+
+ ```conf
+ ## Define a `bind` address pointing to a local IP that your other machines
+ ## can reach you. If you really need to bind to an external accessible IP, make
+ ## sure you add extra firewall rules to prevent unauthorized access:
+ bind 10.0.0.1
+
+ ## Define a `port` to force redis to listen on TCP so other machines can
+ ## connect to it (default port is `6379`).
+ port 6379
+
+ ## Set up password authentication (use the same password in all nodes).
+ ## The password should be defined equal for both `requirepass` and `masterauth`
+ ## when setting up Redis to use with Sentinel.
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+ ```
+
+1. Restart the Redis service for the changes to take effect.
+
+### Step 2. Configuring the slave Redis instances
+
+Assuming that the Redis slave instance IP is `10.0.0.2`:
+
+1. [Install Redis](../../install/installation.md#6-redis)
+1. Edit `/etc/redis/redis.conf`:
+
+ ```conf
+ ## Define a `bind` address pointing to a local IP that your other machines
+ ## can reach you. If you really need to bind to an external accessible IP, make
+ ## sure you add extra firewall rules to prevent unauthorized access:
+ bind 10.0.0.2
+
+ ## Define a `port` to force redis to listen on TCP so other machines can
+ ## connect to it (default port is `6379`).
+ port 6379
+
+ ## Set up password authentication (use the same password in all nodes).
+ ## The password should be defined equal for both `requirepass` and `masterauth`
+ ## when setting up Redis to use with Sentinel.
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+
+ ## Define `slaveof` pointing to the Redis master instance with IP and port.
+ slaveof 10.0.0.1 6379
+ ```
+
+1. Restart the Redis service for the changes to take effect.
+1. Go through the steps again for all the other slave nodes.
+
+### Step 3. Configuring the Redis Sentinel instances
+
+Sentinel is a special type of Redis server. It inherits most of the basic
+configuration options you can define in `redis.conf`, with specific ones
+starting with `sentinel` prefix.
+
+Assuming that the Redis Sentinel is installed on the same instance as Redis
+master with IP `10.0.0.1` (some settings might overlap with the master):
+
+1. [Install Redis Sentinel](http://redis.io/topics/sentinel)
+1. Edit `/etc/redis/sentinel.conf`:
+
+ ```conf
+ ## Define a `bind` address pointing to a local IP that your other machines
+ ## can reach you. If you really need to bind to an external accessible IP, make
+ ## sure you add extra firewall rules to prevent unauthorized access:
+ bind 10.0.0.1
+
+ ## Define a `port` to force Sentinel to listen on TCP so other machines can
+ ## connect to it (default port is `6379`).
+ port 26379
+
+ ## Set up password authentication (use the same password in all nodes).
+ ## The password should be defined equal for both `requirepass` and `masterauth`
+ ## when setting up Redis to use with Sentinel.
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+
+ ## Define with `sentinel auth-pass` the same shared password you have
+ ## defined for both Redis master and slaves instances.
+ sentinel auth-pass gitlab-redis redis-password-goes-here
+
+ ## Define with `sentinel monitor` the IP and port of the Redis
+ ## master node, and the quorum required to start a failover.
+ sentinel monitor gitlab-redis 10.0.0.1 6379 2
+
+ ## Define with `sentinel down-after-milliseconds` the time in `ms`
+ ## that an unresponsive server will be considered down.
+ sentinel down-after-milliseconds gitlab-redis 10000
+
+ ## Define a value for `sentinel failover_timeout` in `ms`. This has multiple
+ ## meanings:
+ ##
+ ## * The time needed to re-start a failover after a previous failover was
+ ## already tried against the same master by a given Sentinel, is two
+ ## times the failover timeout.
+ ##
+ ## * The time needed for a slave replicating to a wrong master according
+ ## to a Sentinel current configuration, to be forced to replicate
+ ## with the right master, is exactly the failover timeout (counting since
+ ## the moment a Sentinel detected the misconfiguration).
+ ##
+ ## * The time needed to cancel a failover that is already in progress but
+ ## did not produced any configuration change (SLAVEOF NO ONE yet not
+ ## acknowledged by the promoted slave).
+ ##
+ ## * The maximum time a failover in progress waits for all the slaves to be
+ ## reconfigured as slaves of the new master. However even after this time
+ ## the slaves will be reconfigured by the Sentinels anyway, but not with
+ ## the exact parallel-syncs progression as specified.
+ sentinel failover_timeout 30000
+ ```
+1. Restart the Redis service for the changes to take effect.
+1. Go through the steps again for all the other Sentinel nodes.
+
+### Step 4. Configuring the GitLab application
+
+You can enable or disable Sentinel support at any time in new or existing
+installations. From the GitLab application perspective, all it requires is
+the correct credentials for the Sentinel nodes.
+
+While it doesn't require a list of all Sentinel nodes, in case of a failure,
+it needs to access at least one of listed ones.
+
+The following steps should be performed in the [GitLab application server](gitlab.md)
+which ideally should not have Redis or Sentinels in the same machine for a HA
+setup:
+
+1. Edit `/home/git/gitlab/config/resque.yml` following the example in
+ [resque.yml.example][resque], and uncomment the Sentinel lines, pointing to
+ the correct server credentials:
+
+ ```yaml
+ # resque.yaml
+ production:
+ url: redis://:redi-password-goes-here@gitlab-redis/
+ sentinels:
+ -
+ host: 10.0.0.1
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.2
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.3
+ port: 26379 # point to sentinel, not to redis port
+ ```
+
+1. [Restart GitLab][restart] for the changes to take effect.
+
+## Example of minimal configuration with 1 master, 2 slaves and 3 Sentinels
+
+In this example we consider that all servers have an internal network
+interface with IPs in the `10.0.0.x` range, and that they can connect
+to each other using these IPs.
+
+In a real world usage, you would also setup firewall rules to prevent
+unauthorized access from other machines, and block traffic from the
+outside ([Internet][it]).
+
+For this example, **Sentinel 1** will be configured in the same machine as the
+**Redis Master**, **Sentinel 2** and **Sentinel 3** in the same machines as the
+**Slave 1** and **Slave 2** respectively.
+
+Here is a list and description of each **machine** and the assigned **IP**:
+
+* `10.0.0.1`: Redis Master + Sentinel 1
+* `10.0.0.2`: Redis Slave 1 + Sentinel 2
+* `10.0.0.3`: Redis Slave 2 + Sentinel 3
+* `10.0.0.4`: GitLab application
+
+Please note that after the initial configuration, if a failover is initiated
+by the Sentinel nodes, the Redis nodes will be reconfigured and the **Master**
+will change permanently (including in `redis.conf`) from one node to the other,
+until a new failover is initiated again.
+
+The same thing will happen with `sentinel.conf` that will be overridden after the
+initial execution, after any new sentinel node starts watching the **Master**,
+or a failover promotes a different **Master** node.
+
+### Example configuration for Redis master and Sentinel 1
+
+1. In `/etc/redis/redis.conf`:
+
+ ```conf
+ bind 10.0.0.1
+ port 6379
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+ ```
+
+1. In `/etc/redis/sentinel.conf`:
+
+ ```conf
+ bind 10.0.0.1
+ port 26379
+ sentinel auth-pass gitlab-redis redis-password-goes-here
+ sentinel monitor gitlab-redis 10.0.0.1 6379 2
+ sentinel down-after-milliseconds gitlab-redis 10000
+ sentinel failover_timeout 30000
+ ```
+
+1. Restart the Redis service for the changes to take effect.
+
+### Example configuration for Redis slave 1 and Sentinel 2
+
+1. In `/etc/redis/redis.conf`:
+
+ ```conf
+ bind 10.0.0.2
+ port 6379
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+ slaveof 10.0.0.1 6379
+ ```
+
+1. In `/etc/redis/sentinel.conf`:
+
+ ```conf
+ bind 10.0.0.2
+ port 26379
+ sentinel auth-pass gitlab-redis redis-password-goes-here
+ sentinel monitor gitlab-redis 10.0.0.1 6379 2
+ sentinel down-after-milliseconds gitlab-redis 10000
+ sentinel failover_timeout 30000
+ ```
+
+1. Restart the Redis service for the changes to take effect.
+
+### Example configuration for Redis slave 2 and Sentinel 3
+
+1. In `/etc/redis/redis.conf`:
+
+ ```conf
+ bind 10.0.0.3
+ port 6379
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+ slaveof 10.0.0.1 6379
+ ```
+
+1. In `/etc/redis/sentinel.conf`:
+
+ ```conf
+ bind 10.0.0.3
+ port 26379
+ sentinel auth-pass gitlab-redis redis-password-goes-here
+ sentinel monitor gitlab-redis 10.0.0.1 6379 2
+ sentinel down-after-milliseconds gitlab-redis 10000
+ sentinel failover_timeout 30000
+ ```
+
+1. Restart the Redis service for the changes to take effect.
+
+### Example configuration of the GitLab application
+
+1. Edit `/home/git/gitlab/config/resque.yml`:
+
+ ```yaml
+ production:
+ url: redis://:redi-password-goes-here@gitlab-redis/
+ sentinels:
+ -
+ host: 10.0.0.1
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.2
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.3
+ port: 26379 # point to sentinel, not to redis port
+ ```
+
+1. [Restart GitLab][restart] for the changes to take effect.
+
+## Troubleshooting
+
+We have a more detailed [Troubleshooting](redis.md#troubleshooting) explained
+in the documentation for Omnibus GitLab installations. Here we will list only
+the things that are specific to a source installation.
+
+If you get an error in GitLab like `Redis::CannotConnectError: No sentinels available.`,
+there may be something wrong with your configuration files or it can be related
+to [this upstream issue][gh-531].
+
+You must make sure that `resque.yml` and `sentinel.conf` are configured correctly,
+otherwise `redis-rb` will not work properly.
+
+The `master-group-name` ('gitlab-redis') defined in (`sentinel.conf`)
+**must** be used as the hostname in GitLab (`resque.yml`):
+
+```conf
+# sentinel.conf:
+sentinel monitor gitlab-redis 10.0.0.1 6379 2
+sentinel down-after-milliseconds gitlab-redis 10000
+sentinel config-epoch gitlab-redis 0
+sentinel leader-epoch gitlab-redis 0
+```
+
+```yaml
+# resque.yaml
+production:
+ url: redis://:myredispassword@gitlab-redis/
+ sentinels:
+ -
+ host: 10.0.0.1
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.2
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.3
+ port: 26379 # point to sentinel, not to redis port
+```
+
+When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics/sentinel).
+
+[gh-531]: https://github.com/redis/redis-rb/issues/531
+[downloads]: https://about.gitlab.com/downloads
+[restart]: ../restart_gitlab.md#installations-from-source
+[it]: https://gitlab.com/gitlab-org/gitlab-ce/uploads/c4cc8cd353604bd80315f9384035ff9e/The_Internet_IT_Crowd.png
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 0b5f7778fc7..f68eeb9f86b 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -240,3 +240,21 @@ Example response:
"branch_name": "newbranch"
}
```
+
+## Delete merged branches
+
+Will delete all branches that are merged into the project's default branch.
+
+```
+DELETE /projects/:id/repository/merged_branches
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+
+It returns `200` to indicate deletion of all merged branches was started.
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/merged_branches"
+```
diff --git a/doc/api/builds.md b/doc/api/builds.md
index 0476cac0eda..bca2f9e44ef 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -45,7 +45,7 @@ Example of response
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
- }
+ },
"ref": "master",
"runner": null,
"stage": "test",
@@ -89,7 +89,7 @@ Example of response
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
- }
+ },
"ref": "master",
"runner": null,
"stage": "test",
@@ -163,7 +163,7 @@ Example of response
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
- }
+ },
"ref": "master",
"runner": null,
"stage": "test",
@@ -193,7 +193,7 @@ Example of response
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
- }
+ },
"ref": "master",
"runner": null,
"stage": "test",
@@ -260,7 +260,7 @@ Example of response
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
- }
+ },
"ref": "master",
"runner": null,
"stage": "test",
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index b6cca5d4e2a..bcf8b955044 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -13,44 +13,58 @@ Parameters:
- `id` (required) - The ID of a project
- `path` (optional) - The path inside repository. Used to get contend of subdirectories
- `ref_name` (optional) - The name of a repository branch or tag or if not given the default branch
+- `recursive` (optional) - Boolean value used to get a recursive tree (false by default)
```json
[
{
- "name": "assets",
+ "id": "a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba",
+ "name": "html",
"type": "tree",
- "mode": "040000",
- "id": "6229c43a7e16fcc7e95f923f8ddadb8281d9c6c6"
+ "path": "files/html",
+ "mode": "040000"
},
{
- "name": "contexts",
+ "id": "4535904260b1082e14f867f7a24fd8c21495bde3",
+ "name": "images",
"type": "tree",
- "mode": "040000",
- "id": "faf1cdf33feadc7973118ca42d35f1e62977e91f"
+ "path": "files/images",
+ "mode": "040000"
},
{
- "name": "controllers",
+ "id": "31405c5ddef582c5a9b7a85230413ff90e2fe720",
+ "name": "js",
"type": "tree",
- "mode": "040000",
- "id": "95633e8d258bf3dfba3a5268fb8440d263218d74"
+ "path": "files/js",
+ "mode": "040000"
},
{
- "name": "Rakefile",
- "type": "blob",
- "mode": "100644",
- "id": "35b2f05cbb4566b71b34554cf184a9d0bd9d46d6"
+ "id": "cc71111cfad871212dc99572599a568bfe1e7e00",
+ "name": "lfs",
+ "type": "tree",
+ "path": "files/lfs",
+ "mode": "040000"
},
{
- "name": "VERSION",
- "type": "blob",
- "mode": "100644",
- "id": "803e4a4f3727286c3093c63870c2b6524d30ec4f"
+ "id": "fd581c619bf59cfdfa9c8282377bb09c2f897520",
+ "name": "markdown",
+ "type": "tree",
+ "path": "files/markdown",
+ "mode": "040000"
+ },
+ {
+ "id": "23ea4d11a4bdd960ee5320c5cb65b5b3fdbc60db",
+ "name": "ruby",
+ "type": "tree",
+ "path": "files/ruby",
+ "mode": "040000"
},
{
- "name": "config.ru",
+ "id": "7d70e02340bac451f281cecf0a980907974bd8be",
+ "name": "whitespace",
"type": "blob",
- "mode": "100644",
- "id": "dfd2d862237323aa599be31b473d70a8a817943b"
+ "path": "files/whitespace",
+ "mode": "100644"
}
]
```
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 84048f1d25f..cf7c55f75f2 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -58,6 +58,22 @@ below.
See the [Examples](#examples) section for more details on how to actually
trigger a rebuild.
+## Trigger a build from webhook
+
+> Introduced in GitLab 8.14.
+
+To trigger a build from webhook of another project you need to add the following
+webhook url for Push and Tag push events:
+
+```
+https://gitlab.example.com/api/v3/projects/:id/ref/:ref/trigger/builds?token=TOKEN
+```
+
+> **Note**:
+- `ref` should be passed as part of url in order to take precedence over `ref`
+ from webhook body that designates the branchref that fired the trigger in the source repository.
+- `ref` should be url encoded if contains slashes.
+
## Pass build variables to a trigger
You can pass any number of arbitrary variables in the trigger API call and they
@@ -169,6 +185,14 @@ curl --request POST \
https://gitlab.example.com/api/v3/projects/9/trigger/builds
```
+### Using webhook to trigger builds
+
+You can add the following webhook to another project in order to trigger a build:
+
+```
+https://gitlab.example.com/api/v3/projects/9/ref/master/trigger/builds?token=TOKEN&variables[UPLOAD_TO_S3]=true
+```
+
### Using cron to trigger nightly builds
Whether you craft a script or just run cURL directly, you can trigger builds
diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md
new file mode 100644
index 00000000000..03392a003ee
--- /dev/null
+++ b/doc/development/ux_guide/copy.md
@@ -0,0 +1,78 @@
+# Copy
+
+The copy and messaging is a core part of the experience of GitLab and the conversation with our users. Follow the below conventions throughout GitLab.
+
+>**Note:**
+We are currently inconsistent with this guidance. Images below are created to illustrate the point. As this guidance is refined, we will ensure that our experiences align.
+
+## Contents
+* [Brevity](#brevity)
+* [Forms](#forms)
+* [Terminology](#terminology)
+
+---
+
+## Brevity
+Users will skim content, rather than read text carefully.
+When familiar with a web app, users rely on muscle memory, and may read even less when moving quickly.
+A good experience should quickly orient a user, regardless of their experience, to the purpose of the current screen. This should happen without the user having to consciously read long strings of text.
+In general, text is burdensome and adds cognitive load. This is especially pronounced in a powerful productivity tool such as GitLab.
+We should _not_ rely on words as a crutch to explain the purpose of a screen.
+The current navigation and composition of the elements on the screen should get the user 95% there, with the remaining 5% being specific elements such as text.
+This means that, as a rule, copy should be very short. A long message or label is a red flag hinting at design that needs improvement.
+
+>**Example:**
+Use `Add` instead of `Add issue` as a button label.
+Preferrably use context and placement of controls to make it obvious what clicking on them will do.
+
+---
+
+## Forms
+
+### Adding items
+
+When viewing a list of issues, there is a button that is labeled `Add`. Given the context in the example, it is clearly referring to issues. If the context were not clear enough, the label could be `Add issue`. Clicking the button will bring you to the `Add issue` form. Other add flows should be similar.
+
+![Add issue button](img/copy-form-addissuebutton.png)
+
+The form should be titled `Add issue`. The submit button should be labeled `Save` or `Submit`. Do not use `Add`, `Create`, `New`, or `Save Changes`. The cancel button should be labeled `Cancel`. Do not use `Back`.
+
+![Add issue form](img/copy-form-addissueform.png)
+
+### Editing items
+
+When in context of an issue, the affordance to edit it is labeled `Edit`. If the context is not clear enough, `Edit issue` could be considered. Other edit flows should be similar.
+
+![Edit issue button](img/copy-form-editissuebutton.png)
+
+The form should be titled `Edit Issue`. The submit button should be labeled `Save`. Do not use `Edit`, `Update`, `New`, or `Save Changes`. The cancel button should be labeled `Cancel`. Do not use `Back`.
+
+![Edit issue form](img/copy-form-editissueform.png)
+
+---
+
+## Terminology
+
+### Issues
+
+#### Adjectives (states)
+
+| Term | Use |
+| ---- | --- |
+| Open | Issue is active |
+| Closed | Issue is no longer active |
+
+>**Example:**
+Use `5 open issues` and do not use `5 pending issues`.
+Only use the adjectives in the table above.
+
+#### Verbs (actions)
+
+| Term | Use |
+| ---- | --- |
+| Add | For adding an issue. Do not use `create` or `new` |
+| View | View an issue |
+| Edit | Edit an issue. Do not use `update` |
+| Close | Closing an issue |
+| Re-open | Re-open an issue. There should never be a need to use `open` as a verb |
+| Delete | Deleting an issue. Do not use `remove` | \ No newline at end of file
diff --git a/doc/development/ux_guide/img/copy-form-addissuebutton.png b/doc/development/ux_guide/img/copy-form-addissuebutton.png
new file mode 100644
index 00000000000..18839d447e8
--- /dev/null
+++ b/doc/development/ux_guide/img/copy-form-addissuebutton.png
Binary files differ
diff --git a/doc/development/ux_guide/img/copy-form-addissueform.png b/doc/development/ux_guide/img/copy-form-addissueform.png
new file mode 100644
index 00000000000..e6838c06eca
--- /dev/null
+++ b/doc/development/ux_guide/img/copy-form-addissueform.png
Binary files differ
diff --git a/doc/development/ux_guide/img/copy-form-editissuebutton.png b/doc/development/ux_guide/img/copy-form-editissuebutton.png
new file mode 100644
index 00000000000..2435820e14f
--- /dev/null
+++ b/doc/development/ux_guide/img/copy-form-editissuebutton.png
Binary files differ
diff --git a/doc/development/ux_guide/img/copy-form-editissueform.png b/doc/development/ux_guide/img/copy-form-editissueform.png
new file mode 100644
index 00000000000..5ddeda33e68
--- /dev/null
+++ b/doc/development/ux_guide/img/copy-form-editissueform.png
Binary files differ
diff --git a/doc/development/ux_guide/index.md b/doc/development/ux_guide/index.md
index 1a61be4ed51..8aed11ebac3 100644
--- a/doc/development/ux_guide/index.md
+++ b/doc/development/ux_guide/index.md
@@ -26,6 +26,11 @@ The GitLab experience is broken apart into several surfaces. Each of these surfa
---
+### [Copy](copy.md)
+Conventions on text and messaging within labels, buttons, and other components.
+
+---
+
### [Features](features.md)
The previous building blocks are combined into complete features in the GitLab UX. Examples include our navigation, filters, search results, and empty states.
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 77bea3d2ceb..ae4387e2577 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -48,7 +48,11 @@ at Super User also has relevant information.
**Omnibus Trusted Chain**
-It is enough to concatenate the certificate to the main trusted certificate:
+[Install the self signed certificate or custom certificate authorities](http://docs.gitlab.com/omnibus/common_installation_problems/README.html#using-self-signed-certificate-or-custom-certificate-authorities)
+in to GitLab Omnibus.
+
+It is enough to concatenate the certificate to the main trusted certificate
+however it may be overwritten during upgrades:
```bash
cat jira.pem >> /opt/gitlab/embedded/ssl/certs/cacert.pem
diff --git a/features/snippet_search.feature b/features/snippet_search.feature
deleted file mode 100644
index 834bd3b2376..00000000000
--- a/features/snippet_search.feature
+++ /dev/null
@@ -1,20 +0,0 @@
-@dashboard
-Feature: Snippet Search
- Background:
- Given I sign in as a user
- And I have public "Personal snippet one" snippet
- And I have private "Personal snippet private" snippet
- And I have a public many lined snippet
-
- Scenario: I should see my public and private snippets
- When I search for "snippet" in snippet titles
- Then I should see "Personal snippet one" in results
- And I should see "Personal snippet private" in results
-
- Scenario: I should see three surrounding lines on either side of a matching snippet line
- When I search for "line seven" in snippet contents
- Then I should see "line four" in results
- And I should see "line seven" in results
- And I should see "line ten" in results
- And I should not see "line three" in results
- And I should not see "line eleven" in results
diff --git a/features/snippets/discover.feature b/features/snippets/discover.feature
deleted file mode 100644
index 1a7e132ea25..00000000000
--- a/features/snippets/discover.feature
+++ /dev/null
@@ -1,13 +0,0 @@
-@snippets
-Feature: Snippets Discover
- Background:
- Given I sign in as a user
- And I have public "Personal snippet one" snippet
- And I have private "Personal snippet private" snippet
- And I have internal "Personal snippet internal" snippet
-
- Scenario: I should see snippets
- Given I visit snippets page
- Then I should see "Personal snippet one" in snippets
- And I should see "Personal snippet internal" in snippets
- And I should not see "Personal snippet private" in snippets
diff --git a/features/steps/shared/search.rb b/features/steps/shared/search.rb
deleted file mode 100644
index 6c3d601763d..00000000000
--- a/features/steps/shared/search.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module SharedSearch
- include Spinach::DSL
-
- def search_snippet_contents(query)
- visit "/search?search=#{URI::encode(query)}&snippets=true&scope=snippet_blobs"
- end
-
- def search_snippet_titles(query)
- visit "/search?search=#{URI::encode(query)}&snippets=true&scope=snippet_titles"
- end
-end
diff --git a/features/steps/snippet_search.rb b/features/steps/snippet_search.rb
deleted file mode 100644
index 32e29ffad1e..00000000000
--- a/features/steps/snippet_search.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-class Spinach::Features::SnippetSearch < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedSnippet
- include SharedUser
- include SharedSearch
-
- step 'I search for "snippet" in snippet titles' do
- search_snippet_titles 'snippet'
- end
-
- step 'I search for "snippet private" in snippet titles' do
- search_snippet_titles 'snippet private'
- end
-
- step 'I search for "line seven" in snippet contents' do
- search_snippet_contents 'line seven'
- end
-
- step 'I should see "line seven" in results' do
- expect(page).to have_content 'line seven'
- end
-
- step 'I should see "line four" in results' do
- expect(page).to have_content 'line four'
- end
-
- step 'I should see "line ten" in results' do
- expect(page).to have_content 'line ten'
- end
-
- step 'I should not see "line eleven" in results' do
- expect(page).not_to have_content 'line eleven'
- end
-
- step 'I should not see "line three" in results' do
- expect(page).not_to have_content 'line three'
- end
-
- step 'I should see "Personal snippet one" in results' do
- expect(page).to have_content 'Personal snippet one'
- end
-
- step 'I should see "Personal snippet private" in results' do
- expect(page).to have_content 'Personal snippet private'
- end
-
- step 'I should not see "Personal snippet one" in results' do
- expect(page).not_to have_content 'Personal snippet one'
- end
-
- step 'I should not see "Personal snippet private" in results' do
- expect(page).not_to have_content 'Personal snippet private'
- end
-end
diff --git a/features/steps/snippets/discover.rb b/features/steps/snippets/discover.rb
deleted file mode 100644
index 76379d09d02..00000000000
--- a/features/steps/snippets/discover.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-class Spinach::Features::SnippetsDiscover < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedSnippet
-
- step 'I should see "Personal snippet one" in snippets' do
- expect(page).to have_content "Personal snippet one"
- end
-
- step 'I should see "Personal snippet internal" in snippets' do
- expect(page).to have_content "Personal snippet internal"
- end
-
- step 'I should not see "Personal snippet private" in snippets' do
- expect(page).not_to have_content "Personal snippet private"
- end
-
- def snippet
- @snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one")
- end
-end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 21a106387f0..73aed624ea7 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -128,6 +128,18 @@ module API
render_api_error!(result[:message], result[:return_code])
end
end
+
+ # Delete all merged branches
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # DELETE /projects/:id/repository/branches/delete_merged
+ delete ":id/repository/merged_branches" do
+ DeleteMergedBranchesService.new(user_project, current_user).async_execute
+
+ status(200)
+ end
end
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 1942aeea656..6e370e961c4 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -159,7 +159,7 @@ module API
end
class RepoTreeObject < Grape::Entity
- expose :id, :name, :type
+ expose :id, :name, :type, :path
expose :mode do |obj, options|
filemode = obj.mode.to_s(8)
@@ -437,7 +437,18 @@ module API
end
class Label < LabelBasic
- expose :open_issues_count, :closed_issues_count, :open_merge_requests_count
+ expose :open_issues_count do |label, options|
+ label.open_issues_count(options[:current_user])
+ end
+
+ expose :closed_issues_count do |label, options|
+ label.closed_issues_count(options[:current_user])
+ end
+
+ expose :open_merge_requests_count do |label, options|
+ label.open_merge_requests_count(options[:current_user])
+ end
+
expose :priority do |label, options|
label.priority(options[:project])
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 40644fc2adf..3f57b9ab5bc 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -1,118 +1,111 @@
module API
- # groups API
class Groups < Grape::API
before { authenticate! }
+ helpers do
+ params :optional_params do
+ optional :description, type: String, desc: 'The description of the group'
+ optional :visibility_level, type: Integer, desc: 'The visibility level of the group'
+ optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
+ optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
+ end
+ end
+
resource :groups do
- # Get a groups list
- #
- # Parameters:
- # skip_groups (optional) - Array of group ids to exclude from list
- # all_available (optional, boolean) - Show all group that you have access to
- # Example Request:
- # GET /groups
+ desc 'Get a groups list' do
+ success Entities::Group
+ end
+ params do
+ optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
+ optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
+ optional :search, type: String, desc: 'Search for a specific group'
+ end
get do
- @groups = if current_user.admin
- Group.all
- elsif params[:all_available]
- GroupsFinder.new.execute(current_user)
- else
- current_user.groups
- end
+ groups = if current_user.admin
+ Group.all
+ elsif params[:all_available]
+ GroupsFinder.new.execute(current_user)
+ else
+ current_user.groups
+ end
- @groups = @groups.search(params[:search]) if params[:search].present?
- @groups = @groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
- @groups = paginate @groups
- present @groups, with: Entities::Group
+ groups = groups.search(params[:search]) if params[:search].present?
+ groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
+ present paginate(groups), with: Entities::Group
end
- # Get list of owned groups for authenticated user
- #
- # Example Request:
- # GET /groups/owned
+ desc 'Get list of owned groups for authenticated user' do
+ success Entities::Group
+ end
get '/owned' do
- @groups = current_user.owned_groups
- @groups = paginate @groups
- present @groups, with: Entities::Group, user: current_user
+ groups = current_user.owned_groups
+ present paginate(groups), with: Entities::Group, user: current_user
end
- # Create group. Available only for users who can create groups.
- #
- # Parameters:
- # name (required) - The name of the group
- # path (required) - The path of the group
- # description (optional) - The description of the group
- # visibility_level (optional) - The visibility level of the group
- # lfs_enabled (optional) - Enable/disable LFS for the projects in this group
- # request_access_enabled (optional) - Allow users to request member access
- # Example Request:
- # POST /groups
+ desc 'Create a group. Available only for users who can create groups.' do
+ success Entities::Group
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the group'
+ requires :path, type: String, desc: 'The path of the group'
+ use :optional_params
+ end
post do
authorize! :create_group
- required_attributes! [:name, :path]
- attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled, :request_access_enabled]
- @group = Group.new(attrs)
+ group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute
- if @group.save
- @group.add_owner(current_user)
- present @group, with: Entities::Group
+ if group.persisted?
+ present group, with: Entities::Group
else
- render_api_error!("Failed to save group #{@group.errors.messages}", 400)
+ render_api_error!("Failed to save group #{group.errors.messages}", 400)
end
end
+ end
- # Update group. Available only for users who can administrate groups.
- #
- # Parameters:
- # id (required) - The ID of a group
- # path (optional) - The path of the group
- # description (optional) - The description of the group
- # visibility_level (optional) - The visibility level of the group
- # lfs_enabled (optional) - Enable/disable LFS for the projects in this group
- # request_access_enabled (optional) - Allow users to request member access
- # Example Request:
- # PUT /groups/:id
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups do
+ desc 'Update a group. Available only for users who can administrate groups.' do
+ success Entities::Group
+ end
+ params do
+ optional :name, type: String, desc: 'The name of the group'
+ optional :path, type: String, desc: 'The path of the group'
+ use :optional_params
+ at_least_one_of :name, :path, :description, :visibility_level,
+ :lfs_enabled, :request_access_enabled
+ end
put ':id' do
group = find_group(params[:id])
authorize! :admin_group, group
- attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled, :request_access_enabled]
-
- if ::Groups::UpdateService.new(group, current_user, attrs).execute
+ if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute
present group, with: Entities::GroupDetail
else
render_validation_error!(group)
end
end
- # Get a single group, with containing projects
- #
- # Parameters:
- # id (required) - The ID of a group
- # Example Request:
- # GET /groups/:id
+ desc 'Get a single group, with containing projects.' do
+ success Entities::GroupDetail
+ end
get ":id" do
group = find_group(params[:id])
present group, with: Entities::GroupDetail
end
- # Remove group
- #
- # Parameters:
- # id (required) - The ID of a group
- # Example Request:
- # DELETE /groups/:id
+ desc 'Remove a group.'
delete ":id" do
group = find_group(params[:id])
authorize! :admin_group, group
DestroyGroupService.new(group, current_user).execute
end
- # Get a list of projects in this group
- #
- # Example Request:
- # GET /groups/:id/projects
+ desc 'Get a list of projects in this group.' do
+ success Entities::Project
+ end
get ":id/projects" do
group = find_group(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user)
@@ -120,13 +113,12 @@ module API
present projects, with: Entities::Project, user: current_user
end
- # Transfer a project to the Group namespace
- #
- # Parameters:
- # id - group id
- # project_id - project id
- # Example Request:
- # POST /groups/:id/projects/:project_id
+ desc 'Transfer a project to the group namespace. Available only for admin.' do
+ success Entities::GroupDetail
+ end
+ params do
+ requires :project_id, type: String, desc: 'The ID of the project'
+ end
post ":id/projects/:project_id" do
authenticated_as_admin!
group = Group.find_by(id: params[:id])
@@ -134,7 +126,7 @@ module API
result = ::Projects::TransferService.new(project, current_user).execute(group)
if result
- present group
+ present group, with: Entities::GroupDetail
else
render_api_error!("Failed to transfer project #{project.errors.messages}", 400)
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 6998b6dc039..84cc9200d1b 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -86,25 +86,10 @@ module API
end
def project_service
- @project_service ||= begin
- underscored_service = params[:service_slug].underscore
-
- if Service.available_services_names.include?(underscored_service)
- user_project.build_missing_services
-
- service_method = "#{underscored_service}_service"
-
- send_service(service_method)
- end
- end
-
+ @project_service ||= user_project.find_or_initialize_service(params[:service_slug].underscore)
@project_service || not_found!("Service")
end
- def send_service(service_method)
- user_project.send(service_method)
- end
-
def service_attributes
@service_attributes ||= project_service.fields.inject([]) do |arr, hash|
arr << hash[:name].to_sym
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
new file mode 100644
index 00000000000..eb223c1101d
--- /dev/null
+++ b/lib/api/helpers/internal_helpers.rb
@@ -0,0 +1,57 @@
+module API
+ module Helpers
+ module InternalHelpers
+ # Project paths may be any of the following:
+ # * /repository/storage/path/namespace/project
+ # * /namespace/project
+ # * namespace/project
+ #
+ # In addition, they may have a '.git' extension and multiple namespaces
+ #
+ # Transform all these cases to 'namespace/project'
+ def clean_project_path(project_path, storage_paths = Repository.storages.values)
+ project_path = project_path.sub(/\.git\z/, '')
+
+ storage_paths.each do |storage_path|
+ storage_path = File.expand_path(storage_path)
+
+ if project_path.start_with?(storage_path)
+ project_path = project_path.sub(storage_path, '')
+ break
+ end
+ end
+
+ project_path.sub(/\A\//, '')
+ end
+
+ def project_path
+ @project_path ||= clean_project_path(params[:project])
+ end
+
+ def wiki?
+ @wiki ||= project_path.end_with?('.wiki') &&
+ !Project.find_with_namespace(project_path)
+ end
+
+ def project
+ @project ||= begin
+ # Check for *.wiki repositories.
+ # Strip out the .wiki from the pathname before finding the
+ # project. This applies the correct project permissions to
+ # the wiki repository as well.
+ project_path.chomp!('.wiki') if wiki?
+
+ Project.find_with_namespace(project_path)
+ end
+ end
+
+ def ssh_authentication_abilities
+ [
+ :read_project,
+ :download_code,
+ :push_code
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index ccf181402f9..7087ce11401 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -3,6 +3,8 @@ module API
class Internal < Grape::API
before { authenticate_by_gitlab_shell_token! }
+ helpers ::API::Helpers::InternalHelpers
+
namespace 'internal' do
# Check if git command is allowed to project
#
@@ -14,42 +16,6 @@ module API
# ref - branch name
# forced_push - forced_push
# protocol - Git access protocol being used, e.g. HTTP or SSH
- #
-
- helpers do
- def project_path
- @project_path ||= begin
- project_path = params[:project].sub(/\.git\z/, '')
- Repository.remove_storage_from_path(project_path)
- end
- end
-
- def wiki?
- @wiki ||= project_path.end_with?('.wiki') &&
- !Project.find_with_namespace(project_path)
- end
-
- def project
- @project ||= begin
- # Check for *.wiki repositories.
- # Strip out the .wiki from the pathname before finding the
- # project. This applies the correct project permissions to
- # the wiki repository as well.
- project_path.chomp!('.wiki') if wiki?
-
- Project.find_with_namespace(project_path)
- end
- end
-
- def ssh_authentication_abilities
- [
- :read_project,
- :download_code,
- :push_code
- ]
- end
- end
-
post "/allowed" do
status 200
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index bf8504e1101..f9720786e63 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -1,8 +1,12 @@
module API
- # MergeRequest API
class MergeRequests < Grape::API
+ DEPRECATION_MESSAGE = 'This endpoint is deprecated and will be removed in GitLab 9.0.'.freeze
+
before { authenticate! }
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
helpers do
def handle_merge_request_errors!(errors)
@@ -18,27 +22,27 @@ module API
render_api_error!(errors, 400)
end
+
+ params :optional_params do
+ optional :description, type: String, desc: 'The description of the merge request'
+ optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
+ optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
+ end
end
- # List merge requests
- #
- # Parameters:
- # id (required) - The ID of a project
- # iid (optional) - Return the project MR having the given `iid`
- # state (optional) - Return requests "merged", "opened" or "closed"
- # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
- # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- #
- # Example:
- # GET /projects/:id/merge_requests
- # GET /projects/:id/merge_requests?state=opened
- # GET /projects/:id/merge_requests?state=closed
- # GET /projects/:id/merge_requests?order_by=created_at
- # GET /projects/:id/merge_requests?order_by=updated_at
- # GET /projects/:id/merge_requests?sort=desc
- # GET /projects/:id/merge_requests?sort=asc
- # GET /projects/:id/merge_requests?iid=42
- #
+ desc 'List merge requests' do
+ success Entities::MergeRequest
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed merged all], default: 'all',
+ desc: 'Return opened, closed, merged, or all merge requests'
+ optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
+ desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return merge requests sorted in `asc` or `desc` order.'
+ optional :iid, type: Integer, desc: 'The IID of the merge requests'
+ end
get ":id/merge_requests" do
authorize! :read_merge_request, user_project
merge_requests = user_project.merge_requests.inc_notes_with_associations
@@ -48,10 +52,10 @@ module API
end
merge_requests =
- case params["state"]
- when "opened" then merge_requests.opened
- when "closed" then merge_requests.closed
- when "merged" then merge_requests.merged
+ case params[:state]
+ when 'opened' then merge_requests.opened
+ when 'closed' then merge_requests.closed
+ when 'merged' then merge_requests.merged
else merge_requests
end
@@ -59,36 +63,28 @@ module API
present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user
end
- # Create MR
- #
- # Parameters:
- #
- # id (required) - The ID of a project - this will be the source of the merge request
- # source_branch (required) - The source branch
- # target_branch (required) - The target branch
- # target_project_id - The target project of the merge request defaults to the :id of the project
- # assignee_id - Assignee user ID
- # title (required) - Title of MR
- # description - Description of MR
- # labels (optional) - Labels for MR as a comma-separated list
- # milestone_id (optional) - Milestone ID
- #
- # Example:
- # POST /projects/:id/merge_requests
- #
+ desc 'Create a merge request' do
+ success Entities::MergeRequest
+ end
+ params do
+ requires :title, type: String, desc: 'The title of the merge request'
+ requires :source_branch, type: String, desc: 'The source branch'
+ requires :target_branch, type: String, desc: 'The target branch'
+ optional :target_project_id, type: Integer,
+ desc: 'The target project of the merge request defaults to the :id of the project'
+ use :optional_params
+ end
post ":id/merge_requests" do
authorize! :create_merge_request, user_project
- required_attributes! [:source_branch, :target_branch, :title]
- attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description, :milestone_id]
+
+ mr_params = declared_params
# Validate label names in advance
- if (errors = validate_label_params(params)).any?
+ if (errors = validate_label_params(mr_params)).any?
render_api_error!({ labels: errors }, 400)
end
- attrs[:labels] = params[:labels] if params[:labels]
-
- merge_request = ::MergeRequests::CreateService.new(user_project, current_user, attrs).execute
+ merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
if merge_request.valid?
present merge_request, with: Entities::MergeRequest, current_user: current_user
@@ -97,11 +93,10 @@ module API
end
end
- # Delete a MR
- #
- # Parameters:
- # id (required) - The ID of the project
- # merge_request_id (required) - The MR id
+ desc 'Delete a merge request'
+ params do
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
delete ":id/merge_requests/:merge_request_id" do
merge_request = user_project.merge_requests.find_by(id: params[:merge_request_id])
@@ -112,89 +107,64 @@ module API
# Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0
# Use "merge_requests/:merge_request_id/..." instead.
#
- [":id/merge_request/:merge_request_id", ":id/merge_requests/:merge_request_id"].each do |path|
- # Show MR
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - The ID of MR
- #
- # Example:
- # GET /projects/:id/merge_requests/:merge_request_id
- #
+ params do
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
+ { ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status|
+ desc 'Get a single merge request' do
+ if status == :deprecated
+ detail DEPRECATION_MESSAGE
+ end
+ success Entities::MergeRequest
+ end
get path do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
-
authorize! :read_merge_request, merge_request
-
present merge_request, with: Entities::MergeRequest, current_user: current_user
end
- # Show MR commits
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - The ID of MR
- #
- # Example:
- # GET /projects/:id/merge_requests/:merge_request_id/commits
- #
+ desc 'Get the commits of a merge request' do
+ success Entities::RepoCommit
+ end
get "#{path}/commits" do
- merge_request = user_project.merge_requests.
- find(params[:merge_request_id])
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request.commits, with: Entities::RepoCommit
end
- # Show MR changes
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - The ID of MR
- #
- # Example:
- # GET /projects/:id/merge_requests/:merge_request_id/changes
- #
+ desc 'Show the merge request changes' do
+ success Entities::MergeRequestChanges
+ end
get "#{path}/changes" do
- merge_request = user_project.merge_requests.
- find(params[:merge_request_id])
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end
- # Update MR
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # target_branch - The target branch
- # assignee_id - Assignee user ID
- # title - Title of MR
- # state_event - Status of MR. (close|reopen|merge)
- # description - Description of MR
- # labels (optional) - Labels for a MR as a comma-separated list
- # milestone_id (optional) - Milestone ID
- # Example:
- # PUT /projects/:id/merge_requests/:merge_request_id
- #
+ desc 'Update a merge request' do
+ success Entities::MergeRequest
+ end
+ params do
+ optional :title, type: String, desc: 'The title of the merge request'
+ optional :target_branch, type: String, desc: 'The target branch'
+ optional :state_event, type: String, values: %w[close reopen merge],
+ desc: 'Status of the merge request'
+ use :optional_params
+ at_least_one_of :title, :target_branch, :description, :assignee_id,
+ :milestone_id, :labels, :state_event
+ end
put path do
- attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description, :milestone_id]
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = user_project.merge_requests.find(params.delete(:merge_request_id))
authorize! :update_merge_request, merge_request
- # Ensure source_branch is not specified
- if params[:source_branch].present?
- render_api_error!('Source branch cannot be changed', 400)
- end
+ mr_params = declared_params(include_missing: false)
# Validate label names in advance
- if (errors = validate_label_params(params)).any?
+ if (errors = validate_label_params(mr_params)).any?
render_api_error!({ labels: errors }, 400)
end
- attrs[:labels] = params[:labels] if params[:labels]
-
- merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request)
+ merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
if merge_request.valid?
present merge_request, with: Entities::MergeRequest, current_user: current_user
@@ -203,18 +173,17 @@ module API
end
end
- # Merge MR
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # merge_commit_message (optional) - Custom merge commit message
- # should_remove_source_branch (optional) - When true, the source branch will be deleted if possible
- # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds
- # sha (optional) - When present, must have the HEAD SHA of the source branch
- # Example:
- # PUT /projects/:id/merge_requests/:merge_request_id/merge
- #
+ desc 'Merge a merge request' do
+ success Entities::MergeRequest
+ end
+ params do
+ optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
+ optional :should_remove_source_branch, type: Boolean,
+ desc: 'When true, the source branch will be deleted if possible'
+ optional :merge_when_build_succeeds, type: Boolean,
+ desc: 'When true, this merge request will be merged when the build succeeds'
+ optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
+ end
put "#{path}/merge" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
@@ -235,7 +204,7 @@ module API
should_remove_source_branch: params[:should_remove_source_branch]
}
- if to_boolean(params[:merge_when_build_succeeds]) && merge_request.pipeline && merge_request.pipeline.active?
+ if params[:merge_when_build_succeeds] && merge_request.pipeline && merge_request.pipeline.active?
::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params).
execute(merge_request)
else
@@ -246,11 +215,9 @@ module API
present merge_request, with: Entities::MergeRequest, current_user: current_user
end
- # Cancel Merge if Merge When build succeeds is enabled
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- #
+ desc 'Cancel merge if "Merge when build succeeds" is enabled' do
+ success Entities::MergeRequest
+ end
post "#{path}/cancel_merge_when_build_succeeds" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
@@ -259,17 +226,10 @@ module API
::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user).cancel(merge_request)
end
- # Duplicate. DEPRECATED and WILL BE REMOVED in 9.0.
- # Use GET "/projects/:id/merge_requests/:merge_request_id/notes" instead
- #
- # Get a merge request's comments
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # Examples:
- # GET /projects/:id/merge_requests/:merge_request_id/comments
- #
+ desc 'Get the comments of a merge request' do
+ detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0'
+ success Entities::MRNote
+ end
get "#{path}/comments" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
@@ -278,23 +238,15 @@ module API
present paginate(merge_request.notes.fresh), with: Entities::MRNote
end
- # Duplicate. DEPRECATED and WILL BE REMOVED in 9.0.
- # Use POST "/projects/:id/merge_requests/:merge_request_id/notes" instead
- #
- # Post comment to merge request
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # note (required) - Text of comment
- # Examples:
- # POST /projects/:id/merge_requests/:merge_request_id/comments
- #
+ desc 'Post a comment to a merge request' do
+ detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0'
+ success Entities::MRNote
+ end
+ params do
+ requires :note, type: String, desc: 'The text of the comment'
+ end
post "#{path}/comments" do
- required_attributes! [:note]
-
merge_request = user_project.merge_requests.find(params[:merge_request_id])
-
authorize! :create_note, merge_request
opts = {
@@ -312,13 +264,9 @@ module API
end
end
- # List issues that will close on merge
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # Examples:
- # GET /projects/:id/merge_requests/:merge_request_id/closes_issues
+ desc 'List issues that will be closed on merge' do
+ success Entities::MRNote
+ end
get "#{path}/closes_issues" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index c5c214d4d13..b255b47742b 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -5,23 +5,23 @@ module API
NOTEABLE_TYPES = [Issue, MergeRequest, Snippet]
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
NOTEABLE_TYPES.each do |noteable_type|
noteables_str = noteable_type.to_s.underscore.pluralize
- noteable_id_str = "#{noteable_type.to_s.underscore}_id"
-
- # Get a list of project +noteable+ notes
- #
- # Parameters:
- # id (required) - The ID of a project
- # noteable_id (required) - The ID of an issue or snippet
- # Example Request:
- # GET /projects/:id/issues/:noteable_id/notes
- # GET /projects/:id/snippets/:noteable_id/notes
- get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do
- @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym])
-
- if can?(current_user, noteable_read_ability_name(@noteable), @noteable)
+
+ desc 'Get a list of project +noteable+ notes' do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ end
+ get ":id/#{noteables_str}/:noteable_id/notes" do
+ noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+
+ if can?(current_user, noteable_read_ability_name(noteable), noteable)
# We exclude notes that are cross-references and that cannot be viewed
# by the current user. By doing this exclusion at this level and not
# at the DB query level (which we cannot in that case), the current
@@ -31,7 +31,7 @@ module API
# paginate() only works with a relation. This could lead to a
# mismatch between the pagination headers info and the actual notes
# array returned, but this is really a edge-case.
- paginate(@noteable.notes).
+ paginate(noteable.notes).
reject { |n| n.cross_reference_not_visible_for?(current_user) }
present notes, with: Entities::Note
else
@@ -39,44 +39,40 @@ module API
end
end
- # Get a single +noteable+ note
- #
- # Parameters:
- # id (required) - The ID of a project
- # noteable_id (required) - The ID of an issue or snippet
- # note_id (required) - The ID of a note
- # Example Request:
- # GET /projects/:id/issues/:noteable_id/notes/:note_id
- # GET /projects/:id/snippets/:noteable_id/notes/:note_id
- get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do
- @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym])
- @note = @noteable.notes.find(params[:note_id])
- can_read_note = can?(current_user, noteable_read_ability_name(@noteable), @noteable) && !@note.cross_reference_not_visible_for?(current_user)
+ desc 'Get a single +noteable+ note' do
+ success Entities::Note
+ end
+ params do
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ end
+ get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
+ noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+ note = noteable.notes.find(params[:note_id])
+ can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
if can_read_note
- present @note, with: Entities::Note
+ present note, with: Entities::Note
else
not_found!("Note")
end
end
- # Create a new +noteable+ note
- #
- # Parameters:
- # id (required) - The ID of a project
- # noteable_id (required) - The ID of an issue or snippet
- # body (required) - The content of a note
- # created_at (optional) - The date
- # Example Request:
- # POST /projects/:id/issues/:noteable_id/notes
- # POST /projects/:id/snippets/:noteable_id/notes
- post ":id/#{noteables_str}/:#{noteable_id_str}/notes" do
+ desc 'Create a new +noteable+ note' do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :body, type: String, desc: 'The content of a note'
+ optional :created_at, type: String, desc: 'The creation date of the note'
+ end
+ post ":id/#{noteables_str}/:noteable_id/notes" do
required_attributes! [:body]
opts = {
note: params[:body],
noteable_type: noteables_str.classify,
- noteable_id: params[noteable_id_str]
+ noteable_id: params[:noteable_id]
}
if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
@@ -92,19 +88,15 @@ module API
end
end
- # Modify existing +noteable+ note
- #
- # Parameters:
- # id (required) - The ID of a project
- # noteable_id (required) - The ID of an issue or snippet
- # node_id (required) - The ID of a note
- # body (required) - New content of a note
- # Example Request:
- # PUT /projects/:id/issues/:noteable_id/notes/:note_id
- # PUT /projects/:id/snippets/:noteable_id/notes/:node_id
- put ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do
- required_attributes! [:body]
-
+ desc 'Update an existing +noteable+ note' do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ requires :body, type: String, desc: 'The content of a note'
+ end
+ put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
note = user_project.notes.find(params[:note_id])
authorize! :admin_note, note
@@ -113,25 +105,23 @@ module API
note: params[:body]
}
- @note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note)
+ note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note)
- if @note.valid?
- present @note, with: Entities::Note
+ if note.valid?
+ present note, with: Entities::Note
else
render_api_error!("Failed to save note #{note.errors.messages}", 400)
end
end
- # Delete a +noteable+ note
- #
- # Parameters:
- # id (required) - The ID of a project
- # noteable_id (required) - The ID of an issue, MR, or snippet
- # node_id (required) - The ID of a note
- # Example Request:
- # DELETE /projects/:id/issues/:noteable_id/notes/:note_id
- # DELETE /projects/:id/snippets/:noteable_id/notes/:node_id
- delete ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do
+ desc 'Delete a +noteable+ note' do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ end
+ delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
note = user_project.notes.find(params[:note_id])
authorize! :admin_note, note
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index f55aceed92c..0bb2f74809a 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -21,16 +21,18 @@ module API
# Parameters:
# id (required) - The ID of a project
# ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used
+ # recursive (optional) - Used to get a recursive tree
# Example Request:
# GET /projects/:id/repository/tree
get ':id/repository/tree' do
ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
path = params[:path] || nil
+ recursive = to_boolean(params[:recursive])
commit = user_project.commit(ref)
not_found!('Tree') unless commit
- tree = user_project.repository.tree(commit.id, path)
+ tree = user_project.repository.tree(commit.id, path, recursive: recursive)
present tree.sorted_entries, with: Entities::RepoTreeObject
end
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index 9a4f1cd342f..569598fbd2c 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -12,7 +12,7 @@ module API
requires :token, type: String, desc: 'The unique token of trigger'
optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end
- post ":id/trigger/builds" do
+ post ":id/(ref/:ref/)trigger/builds" do
project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id])
trigger = Ci::Trigger.find_by_token(params[:token].to_s)
not_found! unless project && trigger
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 5110bfbf898..c6bf25b5874 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -55,6 +55,12 @@ module Gitlab
repository.commit(deleted_file ? old_ref : new_ref)
end
+ def old_content_commit
+ return unless diff_refs
+
+ repository.commit(old_ref)
+ end
+
def old_ref
diff_refs.try(:base_sha)
end
@@ -111,13 +117,10 @@ module Gitlab
diff_lines.count(&:removed?)
end
- def old_blob(commit = content_commit)
+ def old_blob(commit = old_content_commit)
return unless commit
- parent_id = commit.parent_id
- return unless parent_id
-
- repository.blob_at(parent_id, old_path)
+ repository.blob_at(commit.id, old_path)
end
def blob(commit = content_commit)
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index 0a91d3918d5..a8b4dc2a83f 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -102,6 +102,8 @@ module Gitlab
Gitlab::LDAP::Config.providers.each do |provider|
adapter = Gitlab::LDAP::Adapter.new(provider)
@ldap_person = Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter)
+ # The `uid` might actually be a DN. Try it next.
+ @ldap_person ||= Gitlab::LDAP::Person.find_by_dn(auth_hash.uid, adapter)
break if @ldap_person
end
@ldap_person
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index b8326a64b22..66e6b29e798 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -5,7 +5,7 @@ module Gitlab
def initialize(current_user, project, query, repository_ref = nil)
@current_user = current_user
@project = project
- @repository_ref = repository_ref.presence
+ @repository_ref = repository_ref.presence || project.default_branch
@query = query
end
@@ -40,10 +40,57 @@ module Gitlab
@commits_count ||= commits.count
end
+ def self.parse_search_result(result)
+ ref = nil
+ filename = nil
+ basename = nil
+ startline = 0
+
+ result.each_line.each_with_index do |line, index|
+ if line =~ /^.*:.*:\d+:/
+ ref, filename, startline = line.split(':')
+ startline = startline.to_i - index
+ extname = Regexp.escape(File.extname(filename))
+ basename = filename.sub(/#{extname}$/, '')
+ break
+ end
+ end
+
+ data = ""
+
+ result.each_line do |line|
+ data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
+ end
+
+ OpenStruct.new(
+ filename: filename,
+ basename: basename,
+ ref: ref,
+ startline: startline,
+ data: data
+ )
+ end
+
private
def blobs
- @blobs ||= project.repository.search_files(query, repository_ref)
+ @blobs ||= begin
+ blobs = project.repository.search_files_by_content(query, repository_ref).first(100)
+ found_file_names = Set.new
+
+ results = blobs.map do |blob|
+ blob = self.class.parse_search_result(blob)
+ found_file_names << blob.filename
+
+ [blob.filename, blob]
+ end
+
+ project.repository.search_files_by_name(query, repository_ref).first(100).each do |filename|
+ results << [filename, nil] unless found_file_names.include?(filename)
+ end
+
+ results.sort_by(&:first)
+ end
end
def wiki_blobs
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index c7db84dd5f9..60db0192dfd 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Groups::GroupMembersController do
let(:user) { create(:user) }
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :access_requestable) }
describe 'GET index' do
it 'renders index with 200 status code' do
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 644de308c64..f7cf006efd6 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
describe Projects::BranchesController do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:developer) { create(:user) }
before do
- sign_in(user)
-
project.team << [user, :master]
+ project.team << [user, :developer]
allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz'])
allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0'])
@@ -19,6 +19,8 @@ describe Projects::BranchesController do
context "on creation of a new branch" do
before do
+ sign_in(user)
+
post :create,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -68,6 +70,10 @@ describe Projects::BranchesController do
let(:branch) { "1-feature-branch" }
let!(:issue) { create(:issue, project: project) }
+ before do
+ sign_in(user)
+ end
+
it 'redirects' do
post :create,
namespace_id: project.namespace.to_param,
@@ -94,6 +100,10 @@ describe Projects::BranchesController do
describe "POST destroy with HTML format" do
render_views
+ before do
+ sign_in(user)
+ end
+
it 'returns 303' do
post :destroy,
format: :html,
@@ -109,6 +119,8 @@ describe Projects::BranchesController do
render_views
before do
+ sign_in(user)
+
post :destroy,
format: :js,
id: branch,
@@ -139,4 +151,42 @@ describe Projects::BranchesController do
it { expect(response).to have_http_status(404) }
end
end
+
+ describe "DELETE destroy_all_merged" do
+ def destroy_all_merged
+ delete :destroy_all_merged,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ end
+
+ context 'when user is allowed to push' do
+ before do
+ sign_in(user)
+ end
+
+ it 'redirects to branches' do
+ destroy_all_merged
+
+ expect(response).to redirect_to namespace_project_branches_path(project.namespace, project)
+ end
+
+ it 'starts worker to delete merged branches' do
+ expect_any_instance_of(DeleteMergedBranchesService).to receive(:async_execute)
+
+ destroy_all_merged
+ end
+ end
+
+ context 'when user is not allowed to push' do
+ before do
+ sign_in(developer)
+ end
+
+ it 'responds with status 404' do
+ destroy_all_merged
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 2a7523c6512..b52137fbe7e 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -2,7 +2,7 @@ require('spec_helper')
describe Projects::ProjectMembersController do
let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
describe 'GET index' do
it 'renders index with 200 status code' do
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 2d47a6f6c4c..ebd3595ea64 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -15,5 +15,9 @@ FactoryGirl.define do
trait :private do
visibility_level Gitlab::VisibilityLevel::PRIVATE
end
+
+ trait :access_requestable do
+ request_access_enabled true
+ end
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index bfd88a254f1..1166498ddff 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -24,6 +24,10 @@ FactoryGirl.define do
visibility_level Gitlab::VisibilityLevel::PRIVATE
end
+ trait :access_requestable do
+ request_access_enabled true
+ end
+
trait :empty_repo do
after(:create) do |project|
project.create_repository
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
new file mode 100644
index 00000000000..41dcfe439c2
--- /dev/null
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'Navigation bar counter', feature: true, js: true, caching: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
+ let(:issue) { create(:issue, project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ issue.update(assignee: user)
+ merge_request.update(assignee: user)
+ login_as(user)
+ end
+
+ it 'reflects dashboard issues count' do
+ visit issues_dashboard_path
+
+ expect_counters('issues', '1')
+
+ issue.update(assignee: nil)
+ visit issues_dashboard_path
+
+ expect_counters('issues', '1')
+ end
+
+ it 'reflects dashboard merge requests count' do
+ visit merge_requests_dashboard_path
+
+ expect_counters('merge_requests', '1')
+
+ merge_request.update(assignee: nil)
+ visit merge_requests_dashboard_path
+
+ expect_counters('merge_requests', '1')
+ end
+
+ def expect_counters(issuable_type, count)
+ dashboard_count = find('li.active span.badge')
+ nav_count = find(".dashboard-shortcuts-#{issuable_type} span.count")
+
+ expect(nav_count).to have_content(count)
+ expect(dashboard_count).to have_content(count)
+ end
+end
diff --git a/spec/features/groups/members/owner_manages_access_requests_spec.rb b/spec/features/groups/members/owner_manages_access_requests_spec.rb
index d811b05b0c3..dbe150823ba 100644
--- a/spec/features/groups/members/owner_manages_access_requests_spec.rb
+++ b/spec/features/groups/members/owner_manages_access_requests_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Groups > Members > Owner manages access requests', feature: true do
let(:user) { create(:user) }
let(:owner) { create(:user) }
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :access_requestable) }
background do
group.request_access(user)
diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb
index b3baa2ab57c..d8c9c487996 100644
--- a/spec/features/groups/members/user_requests_access_spec.rb
+++ b/spec/features/groups/members/user_requests_access_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Groups > Members > User requests access', feature: true do
let(:user) { create(:user) }
let(:owner) { create(:user) }
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :access_requestable) }
let!(:project) { create(:project, :private, namespace: group) }
background do
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index c68e1ea4af9..584574cc91a 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -67,4 +67,14 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_content('Source branch "non-exist-source" does not exist')
expect(page).to have_content('Target branch "non-exist-target" does not exist')
end
+
+ context 'when a branch contains commits that both delete and add the same image' do
+ it 'renders the diff successfully' do
+ visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'deleted-image-test' })
+
+ click_link "Changes"
+
+ expect(page).to have_content "6049019_460s.jpg"
+ end
+ end
end
diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
index c4ed92d2780..4973e0aee85 100644
--- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
+++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
feature 'Projects > Members > Group requester cannot request access to project', feature: true do
let(:user) { create(:user) }
let(:owner) { create(:user) }
- let(:group) { create(:group, :public) }
- let(:project) { create(:project, :public, namespace: group) }
+ let(:group) { create(:group, :public, :access_requestable) }
+ let(:project) { create(:project, :public, :access_requestable, namespace: group) }
background do
group.add_owner(owner)
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
index d15376931c3..143390b71cd 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Projects > Members > Master manages access requests', feature: true do
let(:user) { create(:user) }
let(:master) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
background do
project.request_access(user)
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 56ede8eb5be..97c42bd7f01 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Projects > Members > User requests access', feature: true do
let(:user) { create(:user) }
let(:master) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :access_requestable) }
background do
project.team << [master, :master]
diff --git a/spec/features/snippets/explore_spec.rb b/spec/features/snippets/explore_spec.rb
new file mode 100644
index 00000000000..10a4597e467
--- /dev/null
+++ b/spec/features/snippets/explore_spec.rb
@@ -0,0 +1,16 @@
+require 'rails_helper'
+
+feature 'Explore Snippets', feature: true do
+ scenario 'User should see snippets that are not private' do
+ public_snippet = create(:personal_snippet, :public)
+ internal_snippet = create(:personal_snippet, :internal)
+ private_snippet = create(:personal_snippet, :private)
+
+ login_as create(:user)
+ visit explore_snippets_path
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).to have_content(internal_snippet.title)
+ expect(page).not_to have_content(private_snippet.title)
+ end
+end
diff --git a/spec/features/snippets/search_snippets_spec.rb b/spec/features/snippets/search_snippets_spec.rb
new file mode 100644
index 00000000000..146cd3af848
--- /dev/null
+++ b/spec/features/snippets/search_snippets_spec.rb
@@ -0,0 +1,66 @@
+require 'rails_helper'
+
+feature 'Search Snippets', feature: true do
+ scenario 'User searches for snippets by title' do
+ public_snippet = create(:personal_snippet, :public, title: 'Beginning and Middle')
+ private_snippet = create(:personal_snippet, :private, title: 'Middle and End')
+
+ login_as private_snippet.author
+ visit dashboard_snippets_path
+
+ page.within '.search' do
+ fill_in 'search', with: 'Middle'
+ click_button 'Go'
+ end
+
+ click_link 'Titles and Filenames'
+
+ expect(page).to have_link(public_snippet.title)
+ expect(page).to have_link(private_snippet.title)
+ end
+
+ scenario 'User searches for snippet contents' do
+ create(:personal_snippet,
+ :public,
+ title: 'Many lined snippet',
+ content: <<-CONTENT.strip_heredoc
+ |line one
+ |line two
+ |line three
+ |line four
+ |line five
+ |line six
+ |line seven
+ |line eight
+ |line nine
+ |line ten
+ |line eleven
+ |line twelve
+ |line thirteen
+ |line fourteen
+ CONTENT
+ )
+
+ login_as create(:user)
+ visit dashboard_snippets_path
+
+ page.within '.search' do
+ fill_in 'search', with: 'line seven'
+ click_button 'Go'
+ end
+
+ expect(page).to have_content('line seven')
+
+ # 3 lines before the matched line should be visible
+ expect(page).to have_content('line six')
+ expect(page).to have_content('line five')
+ expect(page).to have_content('line four')
+ expect(page).not_to have_content('line three')
+
+ # 3 lines after the matched line should be visible
+ expect(page).to have_content('line eight')
+ expect(page).to have_content('line nine')
+ expect(page).to have_content('line ten')
+ expect(page).not_to have_content('line eleven')
+ end
+end
diff --git a/spec/finders/access_requests_finder_spec.rb b/spec/finders/access_requests_finder_spec.rb
index 8cfea9659cb..c7278e971ae 100644
--- a/spec/finders/access_requests_finder_spec.rb
+++ b/spec/finders/access_requests_finder_spec.rb
@@ -3,12 +3,17 @@ require 'spec_helper'
describe AccessRequestsFinder, services: true do
let(:user) { create(:user) }
let(:access_requester) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:group) { create(:group, :public) }
- before do
- project.request_access(access_requester)
- group.request_access(access_requester)
+ let(:project) do
+ create(:empty_project, :public, :access_requestable) do |project|
+ project.request_access(access_requester)
+ end
+ end
+
+ let(:group) do
+ create(:group, :public, :access_requestable) do |group|
+ group.request_access(access_requester)
+ end
end
shared_examples 'a finder returning access requesters' do |method_name|
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
index 10cfb66ec1c..9085cc8debf 100644
--- a/spec/finders/labels_finder_spec.rb
+++ b/spec/finders/labels_finder_spec.rb
@@ -64,6 +64,21 @@ describe LabelsFinder do
expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1]
end
+
+ context 'as an administrator' do
+ it 'does not return labels from another project' do
+ # Purposefully creating a project with _nothing_ associated to it
+ isolated_project = create(:empty_project)
+ admin = create(:admin)
+
+ # project_3 has a label associated to it, which we don't want coming
+ # back when we ask for the isolated project's labels
+ project_3.team << [admin, :reporter]
+ finder = described_class.new(admin, project_id: isolated_project.id)
+
+ expect(finder.execute).to be_empty
+ end
+ end
end
context 'filtering by title' do
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 6703d88e357..ffca1c94da1 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -11,11 +11,11 @@ describe MembersHelper do
describe '#remove_member_message' do
let(:requester) { build(:user) }
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:project_member) { build(:project_member, project: project) }
let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } }
let(:project_member_request) { project.request_access(requester) }
- let(:group) { create(:group) }
+ let(:group) { create(:group, :access_requestable) }
let(:group_member) { build(:group_member, group: group) }
let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } }
let(:group_member_request) { group.request_access(requester) }
@@ -32,10 +32,10 @@ describe MembersHelper do
describe '#remove_member_title' do
let(:requester) { build(:user) }
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:project_member) { build(:project_member, project: project) }
let(:project_member_request) { project.request_access(requester) }
- let(:group) { create(:group) }
+ let(:group) { create(:group, :access_requestable) }
let(:group_member) { build(:group_member, group: group) }
let(:group_member_request) { group.request_access(requester) }
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 64aa41020c9..4b2ca3514f8 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -6,38 +6,6 @@ describe SearchHelper do
str
end
- describe 'parsing result' do
- let(:project) { create(:project) }
- let(:repository) { project.repository }
- let(:results) { repository.search_files('feature', 'master') }
- let(:search_result) { results.first }
-
- subject { helper.parse_search_result(search_result) }
-
- it "returns a valid OpenStruct object" do
- is_expected.to be_an OpenStruct
- expect(subject.filename).to eq('CHANGELOG')
- expect(subject.basename).to eq('CHANGELOG')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(188)
- expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
- end
-
- context "when filename has extension" do
- let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
-
- it { expect(subject.filename).to eq('CONTRIBUTE.md') }
- it { expect(subject.basename).to eq('CONTRIBUTE') }
- end
-
- context "when file under directory" do
- let(:search_result) { "master:a/b/c.md:5:a b c\n" }
-
- it { expect(subject.filename).to eq('a/b/c.md') }
- it { expect(subject.basename).to eq('a/b/c') }
- end
- end
-
describe 'search_autocomplete_source' do
context "with no current user" do
before do
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 0650cb291e5..38475792d93 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -46,4 +46,28 @@ describe Gitlab::Diff::File, lib: true do
expect(diff_file.collapsed?).to eq(false)
end
end
+
+ describe '#old_content_commit' do
+ it 'returns base commit' do
+ old_content_commit = diff_file.old_content_commit
+
+ expect(old_content_commit.id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ end
+ end
+
+ describe '#old_blob' do
+ it 'returns blob of commit of base commit' do
+ old_data = diff_file.old_blob.data
+
+ expect(old_data).to include('raise "System commands must be given as an array of strings"')
+ end
+ end
+
+ describe '#blob' do
+ it 'returns blob of new commit' do
+ data = diff_file.blob.data
+
+ expect(data).to include('raise RuntimeError, "System commands must be given as an array of strings"')
+ end
+ end
end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 78c669e8fa5..fc9e1cb430a 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -137,11 +137,12 @@ describe Gitlab::OAuth::User, lib: true do
allow(ldap_user).to receive(:username) { uid }
allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] }
allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
- allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
end
context "and no account for the LDAP user" do
it "creates a user with dual LDAP and omniauth identities" do
+ allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+
oauth_user.save
expect(gl_user).to be_valid
@@ -159,6 +160,8 @@ describe Gitlab::OAuth::User, lib: true do
context "and LDAP user has an account already" do
let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
it "adds the omniauth identity to the LDAP account" do
+ allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+
oauth_user.save
expect(gl_user).to be_valid
@@ -172,6 +175,24 @@ describe Gitlab::OAuth::User, lib: true do
])
end
end
+
+ context 'when an LDAP person is not found by uid' do
+ it 'tries to find an LDAP person by DN and adds the omniauth identity to the user' do
+ allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil)
+ allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user)
+
+ oauth_user.save
+
+ identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+ expect(identities_as_hash)
+ .to match_array(
+ [
+ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ { provider: 'twitter', extern_uid: uid }
+ ]
+ )
+ end
+ end
end
context "and no corresponding LDAP person" do
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 29abb4d4d07..a0fdad87eee 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -6,22 +6,65 @@ describe Gitlab::ProjectSearchResults, lib: true do
let(:query) { 'hello world' }
describe 'initialize with empty ref' do
- let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, '') }
+ let(:results) { described_class.new(user, project, query, '') }
it { expect(results.project).to eq(project) }
- it { expect(results.repository_ref).to be_nil }
it { expect(results.query).to eq('hello world') }
end
describe 'initialize with ref' do
let(:ref) { 'refs/heads/test' }
- let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, ref) }
+ let(:results) { described_class.new(user, project, query, ref) }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq(ref) }
it { expect(results.query).to eq('hello world') }
end
+ describe 'blob search' do
+ let(:results) { described_class.new(user, project, 'files').objects('blobs') }
+
+ it 'finds by name' do
+ expect(results).to include(["files/images/wm.svg", nil])
+ end
+
+ it 'finds by content' do
+ blob = results.select { |result| result.first == "CHANGELOG" }.flatten.last
+
+ expect(blob.filename).to eq("CHANGELOG")
+ end
+
+ describe 'parsing results' do
+ let(:results) { project.repository.search_files_by_content('feature', 'master') }
+ let(:search_result) { results.first }
+
+ subject { described_class.parse_search_result(search_result) }
+
+ it "returns a valid OpenStruct object" do
+ is_expected.to be_an OpenStruct
+ expect(subject.filename).to eq('CHANGELOG')
+ expect(subject.basename).to eq('CHANGELOG')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(188)
+ expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
+ end
+
+ context "when filename has extension" do
+ let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
+
+ it { expect(subject.filename).to eq('CONTRIBUTE.md') }
+ it { expect(subject.basename).to eq('CONTRIBUTE') }
+ end
+
+ context "when file under directory" do
+ let(:search_result) { "master:a/b/c.md:5:a b c\n" }
+
+ it { expect(subject.filename).to eq('a/b/c.md') }
+ it { expect(subject.basename).to eq('a/b/c') }
+ end
+ end
+ end
+
describe 'confidential issues' do
let(:query) { 'issue' }
let(:author) { create(:user) }
@@ -66,7 +109,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
it 'lists project confidential issues for assignee' do
- results = described_class.new(assignee, project.id, query)
+ results = described_class.new(assignee, project, query)
issues = results.objects('issues')
expect(issues).to include issue
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index f5f3f58613d..932a5dc4862 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -401,7 +401,12 @@ describe Notify do
describe 'project access requested' do
context 'for a project in a user namespace' do
- let(:project) { create(:project, :public).tap { |p| p.team << [p.owner, :master, p.owner] } }
+ let(:project) do
+ create(:empty_project, :public, :access_requestable) do |project|
+ project.team << [project.owner, :master, project.owner]
+ end
+ end
+
let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
@@ -428,7 +433,7 @@ describe Notify do
context 'for a project in a group' do
let(:group_owner) { create(:user) }
let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } }
- let(:project) { create(:project, :public, namespace: group) }
+ let(:project) { create(:empty_project, :public, :access_requestable, namespace: group) }
let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
@@ -454,7 +459,7 @@ describe Notify do
end
describe 'project access denied' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
@@ -474,7 +479,7 @@ describe Notify do
end
describe 'project access changed' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:user) { create(:user) }
let(:project_member) { create(:project_member, project: project, user: user) }
subject { Notify.member_access_granted_email('project', project_member.id) }
@@ -685,7 +690,7 @@ describe Notify do
context 'for a group' do
describe 'group access requested' do
- let(:group) { create(:group) }
+ let(:group) { create(:group, :public, :access_requestable) }
let(:user) { create(:user) }
let(:group_member) do
group.request_access(user)
diff --git a/spec/models/concerns/access_requestable_spec.rb b/spec/models/concerns/access_requestable_spec.rb
index 96eee0e8bdd..4829ef17a20 100644
--- a/spec/models/concerns/access_requestable_spec.rb
+++ b/spec/models/concerns/access_requestable_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe AccessRequestable do
describe 'Group' do
describe '#request_access' do
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :access_requestable) }
let(:user) { create(:user) }
it { expect(group.request_access(user)).to be_a(GroupMember) }
@@ -11,7 +11,7 @@ describe AccessRequestable do
end
describe '#access_requested?' do
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :access_requestable) }
let(:user) { create(:user) }
before { group.request_access(user) }
@@ -22,14 +22,14 @@ describe AccessRequestable do
describe 'Project' do
describe '#request_access' do
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:user) { create(:user) }
it { expect(project.request_access(user)).to be_a(ProjectMember) }
end
describe '#access_requested?' do
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:user) { create(:user) }
before { project.request_access(user) }
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 29a3af68a9b..b684053cd02 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -94,6 +94,7 @@ describe Event, models: true do
let(:admin) { create(:admin) }
let(:issue) { create(:issue, project: project, author: author, assignee: assignee) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+ let(:note_on_commit) { create(:note_on_commit, project: project) }
let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) }
let(:event) { Event.new(project: project, target: target, author_id: author.id) }
@@ -103,6 +104,32 @@ describe Event, models: true do
project.team << [guest, :guest]
end
+ context 'commit note event' do
+ let(:target) { note_on_commit }
+
+ it do
+ aggregate_failures do
+ expect(event.visible_to_user?(non_member)).to eq true
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq true
+ expect(event.visible_to_user?(admin)).to eq true
+ end
+ end
+
+ context 'private project' do
+ let(:project) { create(:empty_project, :private) }
+
+ it do
+ aggregate_failures do
+ expect(event.visible_to_user?(non_member)).to eq false
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq false
+ expect(event.visible_to_user?(admin)).to eq true
+ end
+ end
+ end
+ end
+
context 'issue event' do
context 'for non confidential issues' do
let(:target) { issue }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 47f89f744cb..1613a586a2c 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Group, models: true do
- let!(:group) { create(:group) }
+ let!(:group) { create(:group, :access_requestable) }
describe 'associations' do
it { is_expected.to have_many :projects }
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 7fc6ed1dd54..1a26cee9f3d 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -71,15 +71,25 @@ describe Key, models: true do
context 'callbacks' do
it 'adds new key to authorized_file' do
- @key = build(:personal_key, id: 7)
- expect(GitlabShellWorker).to receive(:perform_async).with(:add_key, @key.shell_id, @key.key)
- @key.save
+ key = build(:personal_key, id: 7)
+ expect(GitlabShellWorker).to receive(:perform_async).with(:add_key, key.shell_id, key.key)
+ key.save!
end
it 'removes key from authorized_file' do
- @key = create(:personal_key)
- expect(GitlabShellWorker).to receive(:perform_async).with(:remove_key, @key.shell_id, @key.key)
- @key.destroy
+ key = create(:personal_key)
+ expect(GitlabShellWorker).to receive(:perform_async).with(:remove_key, key.shell_id, key.key)
+ key.destroy
+ end
+ end
+
+ describe '#key=' do
+ let(:valid_key) do
+ "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com"
+ end
+
+ it 'strips white spaces' do
+ expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key)
end
end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 485121701af..12419d6fd5a 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -57,7 +57,7 @@ describe Member, models: true do
describe 'Scopes & finders' do
before do
- project = create(:empty_project, :public)
+ project = create(:empty_project, :public, :access_requestable)
group = create(:group)
@owner_user = create(:user).tap { |u| group.add_owner(u) }
@owner = group.members.find_by(user_id: @owner_user.id)
@@ -174,7 +174,7 @@ describe Member, models: true do
describe '.add_user' do
%w[project group].each do |source_type|
context "when source is a #{source_type}" do
- let!(:source) { create(source_type, :public) }
+ let!(:source) { create(source_type, :public, :access_requestable) }
let!(:user) { create(:user) }
let!(:admin) { create(:admin) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 0810d06b50f..c74d9c282cf 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -76,7 +76,7 @@ describe Project, models: true do
end
describe '#members & #requesters' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:requester) { create(:user) }
let(:developer) { create(:user) }
before do
@@ -496,9 +496,6 @@ describe Project, models: true do
end
it 'returns nil and does not query services when there is no external issue tracker' do
- project.build_missing_services
- project.reload
-
expect(project).not_to receive(:services)
expect(project.external_issue_tracker).to eq(nil)
@@ -506,9 +503,6 @@ describe Project, models: true do
it 'retrieves external_issue_tracker querying services and cache it when there is external issue tracker' do
ext_project.reload # Factory returns a project with changed attributes
- ext_project.build_missing_services
- ext_project.reload
-
expect(ext_project).to receive(:services).once.and_call_original
2.times { expect(ext_project.external_issue_tracker).to be_a_kind_of(RedmineService) }
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index e0f2dadf189..12228425579 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -137,7 +137,7 @@ describe ProjectTeam, models: true do
describe '#find_member' do
context 'personal project' do
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:requester) { create(:user) }
before do
@@ -155,7 +155,7 @@ describe ProjectTeam, models: true do
end
context 'group project' do
- let(:group) { create(:group) }
+ let(:group) { create(:group, :access_requestable) }
let(:project) { create(:empty_project, group: group) }
let(:requester) { create(:user) }
@@ -200,7 +200,7 @@ describe ProjectTeam, models: true do
let(:requester) { create(:user) }
context 'personal project' do
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
context 'when project is not shared with group' do
before do
@@ -243,7 +243,7 @@ describe ProjectTeam, models: true do
end
context 'group project' do
- let(:group) { create(:group) }
+ let(:group) { create(:group, :access_requestable) }
let(:project) { create(:empty_project, group: group) }
before do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index fe26b4ac18c..2470d504c68 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -393,33 +393,33 @@ describe Repository, models: true do
end
end
- describe "search_files" do
- let(:results) { repository.search_files('feature', 'master') }
+ describe "search_files_by_content" do
+ let(:results) { repository.search_files_by_content('feature', 'master') }
subject { results }
it { is_expected.to be_an Array }
it 'regex-escapes the query string' do
- results = repository.search_files("test\\", 'master')
+ results = repository.search_files_by_content("test\\", 'master')
expect(results.first).not_to start_with('fatal:')
end
it 'properly handles an unmatched parenthesis' do
- results = repository.search_files("test(", 'master')
+ results = repository.search_files_by_content("test(", 'master')
expect(results.first).not_to start_with('fatal:')
end
it 'properly handles when query is not present' do
- results = repository.search_files('', 'master')
+ results = repository.search_files_by_content('', 'master')
expect(results).to match_array([])
end
it 'properly handles query when repo is empty' do
repository = create(:empty_project).repository
- results = repository.search_files('test', 'master')
+ results = repository.search_files_by_content('test', 'master')
expect(results).to match_array([])
end
@@ -432,6 +432,28 @@ describe Repository, models: true do
end
end
+ describe "search_files_by_name" do
+ let(:results) { repository.search_files_by_name('files', 'master') }
+
+ it 'returns result' do
+ expect(results.first).to eq('files/html/500.html')
+ end
+
+ it 'properly handles when query is not present' do
+ results = repository.search_files_by_name('', 'master')
+
+ expect(results).to match_array([])
+ end
+
+ it 'properly handles query when repo is empty' do
+ repository = create(:empty_project).repository
+
+ results = repository.search_files_by_name('test', 'master')
+
+ expect(results).to match_array([])
+ end
+ end
+
describe '#create_ref' do
it 'redirects the call to fetch_ref' do
ref, ref_path = '1', '2'
@@ -1534,14 +1556,4 @@ describe Repository, models: true do
end.to raise_error(Repository::CommitError)
end
end
-
- describe '#remove_storage_from_path' do
- let(:storage_path) { project.repository_storage_path }
- let(:project_path) { project.path_with_namespace }
- let(:full_path) { File.join(storage_path, project_path) }
-
- it { expect(Repository.remove_storage_from_path(full_path)).to eq(project_path) }
- it { expect(Repository.remove_storage_from_path(project_path)).to eq(project_path) }
- it { expect(Repository.remove_storage_from_path(storage_path)).to eq('') }
- end
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 43937a54b2c..b1615a95004 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -53,7 +53,7 @@ describe Service, models: true do
describe "Template" do
describe "for pushover service" do
- let(:service_template) do
+ let!(:service_template) do
PushoverService.create(
template: true,
properties: {
@@ -66,13 +66,9 @@ describe Service, models: true do
let(:project) { create(:project) }
describe 'is prefilled for projects pushover service' do
- before do
- service_template
- project.build_missing_services
- end
-
it "has all fields prefilled" do
- service = project.pushover_service
+ service = project.find_or_initialize_service('pushover')
+
expect(service.template).to eq(false)
expect(service.device).to eq('MyDevice')
expect(service.sound).to eq('mic')
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 3b152e15b61..580ce4a9e0a 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -37,7 +37,7 @@ describe User, models: true do
describe '#group_members' do
it 'does not include group memberships for which user is a requester' do
user = create(:user)
- group = create(:group, :public)
+ group = create(:group, :public, :access_requestable)
group.request_access(user)
expect(user.group_members).to be_empty
@@ -47,7 +47,7 @@ describe User, models: true do
describe '#project_members' do
it 'does not include project memberships for which user is a requester' do
user = create(:user)
- project = create(:project, :public)
+ project = create(:project, :public, :access_requestable)
project.request_access(user)
expect(user.project_members).to be_empty
@@ -490,6 +490,28 @@ describe User, models: true do
end
end
+ describe '.without_projects' do
+ let!(:project) { create(:empty_project, :public, :access_requestable) }
+ let!(:user) { create(:user) }
+ let!(:user_without_project) { create(:user) }
+ let!(:user_without_project2) { create(:user) }
+
+ before do
+ # add user to project
+ project.team << [user, :master]
+
+ # create invite to projet
+ create(:project_member, :developer, project: project, invite_token: '1234', invite_email: 'inviteduser1@example.com')
+
+ # create request to join project
+ project.request_access(user_without_project2)
+ end
+
+ it { expect(User.without_projects).not_to include user }
+ it { expect(User.without_projects).to include user_without_project }
+ it { expect(User.without_projects).to include user_without_project2 }
+ end
+
describe '.not_in_project' do
before do
User.delete_all
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index b467890a403..1a771b3c87a 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -9,19 +9,19 @@ describe API::AccessRequests, api: true do
let(:stranger) { create(:user) }
let(:project) do
- project = create(:project, :public, creator_id: master.id, namespace: master.namespace)
- project.team << [developer, :developer]
- project.team << [master, :master]
- project.request_access(access_requester)
- project
+ create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
+ project.team << [developer, :developer]
+ project.team << [master, :master]
+ project.request_access(access_requester)
+ end
end
let(:group) do
- group = create(:group, :public)
- group.add_developer(developer)
- group.add_owner(master)
- group.request_access(access_requester)
- group
+ create(:group, :public, :access_requestable) do |group|
+ group.add_developer(developer)
+ group.add_owner(master)
+ group.request_access(access_requester)
+ end
end
shared_examples 'GET /:sources/:id/access_requests' do |source_type|
@@ -89,7 +89,7 @@ describe API::AccessRequests, api: true do
context 'when authenticated as a stranger' do
context "when access request is disabled for the #{source_type}" do
before do
- source.update(request_access_enabled: false)
+ source.update_attributes(request_access_enabled: false)
end
it 'returns 403' do
diff --git a/spec/requests/api/api_internal_helpers_spec.rb b/spec/requests/api/api_internal_helpers_spec.rb
new file mode 100644
index 00000000000..be4bc39ada2
--- /dev/null
+++ b/spec/requests/api/api_internal_helpers_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe ::API::Helpers::InternalHelpers do
+ include ::API::Helpers::InternalHelpers
+
+ describe '.clean_project_path' do
+ project = 'namespace/project'
+ namespaced = File.join('namespace2', project)
+
+ {
+ File.join(Dir.pwd, project) => project,
+ File.join(Dir.pwd, namespaced) => namespaced,
+ project => project,
+ namespaced => namespaced,
+ project + '.git' => project,
+ namespaced + '.git' => namespaced,
+ "/" + project => project,
+ "/" + namespaced => namespaced,
+ }.each do |project_path, expected|
+ context project_path do
+ # Relative and absolute storage paths, with and without trailing /
+ ['.', './', Dir.pwd, Dir.pwd + '/'].each do |storage_path|
+ context "storage path is #{storage_path}" do
+ subject { clean_project_path(project_path, [storage_path]) }
+
+ it { is_expected.to eq(expected) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 1711096f4bd..8f605757186 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -299,4 +299,20 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Cannot remove HEAD branch')
end
end
+
+ describe "DELETE /projects/:id/repository/merged_branches" do
+ before do
+ allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
+ end
+
+ it 'returns 200' do
+ delete api("/projects/#{project.id}/repository/merged_branches", user)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns a 403 error if guest' do
+ delete api("/projects/#{project.id}/repository/merged_branches", user2)
+ expect(response).to have_http_status(403)
+ end
+ end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index b29a13b1d8b..d79b204a24e 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -167,7 +167,7 @@ describe API::API, api: true do
end
it 'returns 404 for a non existing group' do
- put api('/groups/1328', user1)
+ put api('/groups/1328', user1), name: new_group_name
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index f0f590b0331..8f1a1f9e827 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -191,6 +191,26 @@ describe API::API, api: true do
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
end
+
+ context 'project as /namespace/project' do
+ it do
+ pull(key, project_with_repo_path('/' + project.path_with_namespace))
+
+ expect(response).to have_http_status(200)
+ expect(json_response["status"]).to be_truthy
+ expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ end
+ end
+
+ context 'project as namespace/project' do
+ it do
+ pull(key, project_with_repo_path(project.path_with_namespace))
+
+ expect(response).to have_http_status(200)
+ expect(json_response["status"]).to be_truthy
+ expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ end
+ end
end
end
@@ -299,7 +319,7 @@ describe API::API, api: true do
context 'project does not exist' do
it do
- pull(key, OpenStruct.new(path_with_namespace: 'gitlab/notexists'))
+ pull(key, project_with_repo_path('gitlab/notexist'))
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
@@ -392,11 +412,17 @@ describe API::API, api: true do
end
end
+ def project_with_repo_path(path)
+ double().tap do |fake_project|
+ allow(fake_project).to receive_message_chain('repository.path_to_repo' => path)
+ end
+ end
+
def pull(key, project, protocol = 'ssh')
post(
api("/internal/allowed"),
key_id: key.id,
- project: project.path_with_namespace,
+ project: project.repository.path_to_repo,
action: 'git-upload-pack',
secret_token: secret_token,
protocol: protocol
@@ -408,7 +434,7 @@ describe API::API, api: true do
api("/internal/allowed"),
changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master',
key_id: key.id,
- project: project.path_with_namespace,
+ project: project.repository.path_to_repo,
action: 'git-receive-pack',
secret_token: secret_token,
protocol: protocol
@@ -420,7 +446,7 @@ describe API::API, api: true do
api("/internal/allowed"),
ref: 'master',
key_id: key.id,
- project: project.path_with_namespace,
+ project: project.repository.path_to_repo,
action: 'git-upload-archive',
secret_token: secret_token,
protocol: 'ssh'
@@ -432,7 +458,7 @@ describe API::API, api: true do
api("/internal/lfs_authenticate"),
key_id: key_id,
secret_token: secret_token,
- project: project.path_with_namespace
+ project: project.repository.path_to_repo
)
end
end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 5d84976c9c3..77dfebf1a98 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -17,6 +17,10 @@ describe API::API, api: true do
group = create(:group)
group_label = create(:group_label, title: 'feature', group: group)
project.update(group: group)
+ create(:labeled_issue, project: project, labels: [group_label], author: user)
+ create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed)
+ create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project )
+
expected_keys = [
'id', 'name', 'color', 'description',
'open_issues_count', 'closed_issues_count', 'open_merge_requests_count',
@@ -30,14 +34,37 @@ describe API::API, api: true do
expect(json_response.size).to eq(3)
expect(json_response.first.keys).to match_array expected_keys
expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name])
- expect(json_response.last['name']).to eq(label1.name)
- expect(json_response.last['color']).to be_present
- expect(json_response.last['description']).to be_nil
- expect(json_response.last['open_issues_count']).to eq(0)
- expect(json_response.last['closed_issues_count']).to eq(0)
- expect(json_response.last['open_merge_requests_count']).to eq(0)
- expect(json_response.last['priority']).to be_nil
- expect(json_response.last['subscribed']).to be_falsey
+
+ label1_response = json_response.find { |l| l['name'] == label1.title }
+ group_label_response = json_response.find { |l| l['name'] == group_label.title }
+ priority_label_response = json_response.find { |l| l['name'] == priority_label.title }
+
+ expect(label1_response['open_issues_count']).to eq(0)
+ expect(label1_response['closed_issues_count']).to eq(1)
+ expect(label1_response['open_merge_requests_count']).to eq(0)
+ expect(label1_response['name']).to eq(label1.name)
+ expect(label1_response['color']).to be_present
+ expect(label1_response['description']).to be_nil
+ expect(label1_response['priority']).to be_nil
+ expect(label1_response['subscribed']).to be_falsey
+
+ expect(group_label_response['open_issues_count']).to eq(1)
+ expect(group_label_response['closed_issues_count']).to eq(0)
+ expect(group_label_response['open_merge_requests_count']).to eq(0)
+ expect(group_label_response['name']).to eq(group_label.name)
+ expect(group_label_response['color']).to be_present
+ expect(group_label_response['description']).to be_nil
+ expect(group_label_response['priority']).to be_nil
+ expect(group_label_response['subscribed']).to be_falsey
+
+ expect(priority_label_response['open_issues_count']).to eq(0)
+ expect(priority_label_response['closed_issues_count']).to eq(0)
+ expect(priority_label_response['open_merge_requests_count']).to eq(1)
+ expect(priority_label_response['name']).to eq(priority_label.name)
+ expect(priority_label_response['color']).to be_present
+ expect(priority_label_response['description']).to be_nil
+ expect(priority_label_response['priority']).to eq(3)
+ expect(priority_label_response['subscribed']).to be_falsey
end
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 493c0a893d1..2c94c86ccfa 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -9,19 +9,19 @@ describe API::Members, api: true do
let(:stranger) { create(:user) }
let(:project) do
- project = create(:project, :public, creator_id: master.id, namespace: master.namespace)
- project.team << [developer, :developer]
- project.team << [master, :master]
- project.request_access(access_requester)
- project
+ create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
+ project.team << [developer, :developer]
+ project.team << [master, :master]
+ project.request_access(access_requester)
+ end
end
let!(:group) do
- group = create(:group, :public)
- group.add_developer(developer)
- group.add_owner(master)
- group.request_access(access_requester)
- group
+ create(:group, :public, :access_requestable) do |group|
+ group.add_developer(developer)
+ group.add_owner(master)
+ group.request_access(access_requester)
+ end
end
shared_examples 'GET /:sources/:id/members' do |source_type|
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index bae4fa11ec2..7b3d1460c90 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -494,12 +494,6 @@ describe API::API, api: true do
expect(json_response['milestone']['id']).to eq(milestone.id)
end
- it "returns 400 when source_branch is specified" do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
- source_branch: "master", target_branch: "master"
- expect(response).to have_http_status(400)
- end
-
it "returns merge_request with renamed target_branch" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
expect(response).to have_http_status(200)
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index c4dc2d9006a..38c8ad34f9d 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -18,6 +18,7 @@ describe API::API, api: true do
it "returns project commits" do
get api("/projects/#{project.id}/repository/tree", user)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -43,6 +44,40 @@ describe API::API, api: true do
end
end
+
+ describe 'GET /projects/:id/repository/tree?recursive=1' do
+ context 'authorized user' do
+ before { project.team << [user2, :reporter] }
+
+ it 'should return recursive project paths tree' do
+ get api("/projects/#{project.id}/repository/tree?recursive=1", user)
+
+ expect(response.status).to eq(200)
+
+ expect(json_response).to be_an Array
+ expect(json_response[4]['name']).to eq('html')
+ expect(json_response[4]['path']).to eq('files/html')
+ expect(json_response[4]['type']).to eq('tree')
+ expect(json_response[4]['mode']).to eq('040000')
+ end
+
+ it 'returns a 404 for unknown ref' do
+ get api("/projects/#{project.id}/repository/tree?ref_name=foo&recursive=1", user)
+ expect(response).to have_http_status(404)
+
+ expect(json_response).to be_an Object
+ json_response['message'] == '404 Tree Not Found'
+ end
+ end
+
+ context "unauthorized user" do
+ it "does not return project commits" do
+ get api("/projects/#{project.id}/repository/tree?recursive=1")
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
describe "GET /projects/:id/repository/blobs/:sha" do
it "gets the raw file contents" do
get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user)
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 375671bca4c..2aadab3cbe1 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -56,8 +56,7 @@ describe API::API, api: true do
# inject some properties into the service
before do
- project.build_missing_services
- service_object = project.send(service_method)
+ service_object = project.find_or_initialize_service(service)
service_object.properties = service_attrs
service_object.save
end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 8ba2eccf66c..c890a51ae42 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -54,6 +54,13 @@ describe API::API do
expect(pipeline.builds.size).to eq(5)
end
+ it 'creates builds on webhook from other gitlab repository and branch' do
+ expect do
+ post api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+ end.to change(project.builds, :count).by(5)
+ expect(response).to have_http_status(201)
+ end
+
it 'returns bad request with no builds created if there\'s no commit for that ref' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
expect(response).to have_http_status(400)
diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb
new file mode 100644
index 00000000000..181488e89c7
--- /dev/null
+++ b/spec/services/delete_merged_branches_service_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe DeleteMergedBranchesService, services: true do
+ subject(:service) { described_class.new(project, project.owner) }
+
+ let(:project) { create(:project) }
+
+ context '#execute' do
+ context 'unprotected branches' do
+ before do
+ service.execute
+ end
+
+ it 'deletes a branch that was merged' do
+ expect(project.repository.branch_names).not_to include('improve/awesome')
+ end
+
+ it 'keeps branch that is unmerged' do
+ expect(project.repository.branch_names).to include('feature')
+ end
+
+ it 'keeps "master"' do
+ expect(project.repository.branch_names).to include('master')
+ end
+ end
+
+ context 'protected branches' do
+ before do
+ create(:protected_branch, name: 'improve/awesome', project: project)
+ service.execute
+ end
+
+ it 'keeps protected branch' do
+ expect(project.repository.branch_names).to include('improve/awesome')
+ end
+ end
+
+ context 'user without rights' do
+ let(:user) { create(:user) }
+
+ it 'cannot execute' do
+ expect { described_class.new(project, user).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+ end
+
+ context '#async_execute' do
+ it 'calls DeleteMergedBranchesWorker async' do
+ expect(DeleteMergedBranchesWorker).to receive(:perform_async)
+
+ service.async_execute
+ end
+ end
+end
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index 7b090343a3e..7d5a66801db 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe Members::ApproveAccessRequestService, services: true do
let(:user) { create(:user) }
let(:access_requester) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:group) { create(:group, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
+ let(:group) { create(:group, :public, :access_requestable) }
let(:opts) { {} }
shared_examples 'a service raising ActiveRecord::RecordNotFound' do
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index 9995f3488af..574df6e0f42 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -26,6 +26,7 @@ describe Members::DestroyService, services: true do
context 'when the given member is an access requester' do
before do
source.members.find_by(user_id: member_user).destroy
+ source.update_attributes(request_access_enabled: true)
source.request_access(member_user)
end
let(:access_requester) { source.requesters.find_by(user_id: member_user) }
diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb
index 0d2d5f03199..853c125dadb 100644
--- a/spec/services/members/request_access_service_spec.rb
+++ b/spec/services/members/request_access_service_spec.rb
@@ -2,8 +2,6 @@ require 'spec_helper'
describe Members::RequestAccessService, services: true do
let(:user) { create(:user) }
- let(:project) { create(:project, :private) }
- let(:group) { create(:group, :private) }
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
@@ -31,27 +29,26 @@ describe Members::RequestAccessService, services: true do
end
context 'when current user cannot request access to the project' do
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:source) { project }
+ %i[project group].each do |source_type|
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { create(source_type, :private) }
+ end
end
+ end
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:source) { group }
+ context 'when access requests are disabled' do
+ %i[project group].each do |source_type|
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { create(source_type, :public) }
+ end
end
end
context 'when current user can request access to the project' do
- before do
- project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- group.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- end
-
- it_behaves_like 'a service creating a access request' do
- let(:source) { project }
- end
-
- it_behaves_like 'a service creating a access request' do
- let(:source) { group }
+ %i[project group].each do |source_type|
+ it_behaves_like 'a service creating a access request' do
+ let(:source) { create(source_type, :public, :access_requestable) }
+ end
end
end
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index e515bc9f89c..0220f7e1db2 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -227,6 +227,16 @@ describe MergeRequests::RefreshService, services: true do
end
end
+ context 'when the source branch is deleted' do
+ it 'does not create a MergeRequestDiff record' do
+ refresh_service = service.new(@project, @user)
+
+ expect do
+ refresh_service.execute(@oldrev, Gitlab::Git::BLANK_SHA, 'refs/heads/master')
+ end.not_to change { MergeRequestDiff.count }
+ end
+ end
+
def reload_mrs
@merge_request.reload
@fork_merge_request.reload
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 876bfaf085c..2cf9883113c 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -10,13 +10,6 @@ describe Projects::CreateService, services: true do
}
end
- it 'creates services on Project creation' do
- project = create_project(@user, @opts)
- project.reload
-
- expect(project.services).not_to be_empty
- end
-
it 'creates labels on Project creation if there are templates' do
Label.create(title: "bug", template: true)
project = create_project(@user, @opts)
@@ -137,6 +130,19 @@ describe Projects::CreateService, services: true do
expect(project.namespace).to eq(@user.namespace)
end
end
+
+ context 'when there is an active service template' do
+ before do
+ create(:service, project: nil, template: true, active: true)
+ end
+
+ it 'creates a service from this template' do
+ project = create_project(@user, @opts)
+ project.reload
+
+ expect(project.services.count).to eq 1
+ end
+ end
end
def create_project(user, opts)
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 103f7542286..4cf81be3adc 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -35,6 +35,7 @@ module TestEnv
'conflict-missing-side' => 'eb227b3',
'conflict-non-utf8' => 'd0a293c',
'conflict-too-large' => '39fa04f',
+ 'deleted-image-test' => '6c17798'
}
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
diff --git a/spec/workers/delete_merged_branches_worker_spec.rb b/spec/workers/delete_merged_branches_worker_spec.rb
new file mode 100644
index 00000000000..d9497bd486c
--- /dev/null
+++ b/spec/workers/delete_merged_branches_worker_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe DeleteMergedBranchesWorker do
+ subject(:worker) { described_class.new }
+
+ let(:project) { create(:project) }
+
+ describe "#perform" do
+ it "calls DeleteMergedBranchesService" do
+ expect_any_instance_of(DeleteMergedBranchesService).to receive(:execute).and_return(true)
+
+ worker.perform(project.id, project.owner.id)
+ end
+
+ it "returns false when project was not found" do
+ expect(worker.perform('unknown', project.owner.id)).to be_falsy
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/es6-promise.auto.js b/vendor/assets/javascripts/es6-promise.auto.js
new file mode 100644
index 00000000000..19e6c13a655
--- /dev/null
+++ b/vendor/assets/javascripts/es6-promise.auto.js
@@ -0,0 +1,1159 @@
+/*!
+ * @overview es6-promise - a tiny implementation of Promises/A+.
+ * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
+ * @license Licensed under MIT license
+ * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE
+ * @version 4.0.5
+ */
+
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.ES6Promise = factory());
+}(this, (function () { 'use strict';
+
+function objectOrFunction(x) {
+ return typeof x === 'function' || typeof x === 'object' && x !== null;
+}
+
+function isFunction(x) {
+ return typeof x === 'function';
+}
+
+var _isArray = undefined;
+if (!Array.isArray) {
+ _isArray = function (x) {
+ return Object.prototype.toString.call(x) === '[object Array]';
+ };
+} else {
+ _isArray = Array.isArray;
+}
+
+var isArray = _isArray;
+
+var len = 0;
+var vertxNext = undefined;
+var customSchedulerFn = undefined;
+
+var asap = function asap(callback, arg) {
+ queue[len] = callback;
+ queue[len + 1] = arg;
+ len += 2;
+ if (len === 2) {
+ // If len is 2, that means that we need to schedule an async flush.
+ // If additional callbacks are queued before the queue is flushed, they
+ // will be processed by this flush that we are scheduling.
+ if (customSchedulerFn) {
+ customSchedulerFn(flush);
+ } else {
+ scheduleFlush();
+ }
+ }
+};
+
+function setScheduler(scheduleFn) {
+ customSchedulerFn = scheduleFn;
+}
+
+function setAsap(asapFn) {
+ asap = asapFn;
+}
+
+var browserWindow = typeof window !== 'undefined' ? window : undefined;
+var browserGlobal = browserWindow || {};
+var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver;
+var isNode = typeof self === 'undefined' && typeof process !== 'undefined' && ({}).toString.call(process) === '[object process]';
+
+// test for web worker but not in IE10
+var isWorker = typeof Uint8ClampedArray !== 'undefined' && typeof importScripts !== 'undefined' && typeof MessageChannel !== 'undefined';
+
+// node
+function useNextTick() {
+ // node version 0.10.x displays a deprecation warning when nextTick is used recursively
+ // see https://github.com/cujojs/when/issues/410 for details
+ return function () {
+ return process.nextTick(flush);
+ };
+}
+
+// vertx
+function useVertxTimer() {
+ if (typeof vertxNext !== 'undefined') {
+ return function () {
+ vertxNext(flush);
+ };
+ }
+
+ return useSetTimeout();
+}
+
+function useMutationObserver() {
+ var iterations = 0;
+ var observer = new BrowserMutationObserver(flush);
+ var node = document.createTextNode('');
+ observer.observe(node, { characterData: true });
+
+ return function () {
+ node.data = iterations = ++iterations % 2;
+ };
+}
+
+// web worker
+function useMessageChannel() {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = flush;
+ return function () {
+ return channel.port2.postMessage(0);
+ };
+}
+
+function useSetTimeout() {
+ // Store setTimeout reference so es6-promise will be unaffected by
+ // other code modifying setTimeout (like sinon.useFakeTimers())
+ var globalSetTimeout = setTimeout;
+ return function () {
+ return globalSetTimeout(flush, 1);
+ };
+}
+
+var queue = new Array(1000);
+function flush() {
+ for (var i = 0; i < len; i += 2) {
+ var callback = queue[i];
+ var arg = queue[i + 1];
+
+ callback(arg);
+
+ queue[i] = undefined;
+ queue[i + 1] = undefined;
+ }
+
+ len = 0;
+}
+
+function attemptVertx() {
+ try {
+ var r = require;
+ var vertx = r('vertx');
+ vertxNext = vertx.runOnLoop || vertx.runOnContext;
+ return useVertxTimer();
+ } catch (e) {
+ return useSetTimeout();
+ }
+}
+
+var scheduleFlush = undefined;
+// Decide what async method to use to triggering processing of queued callbacks:
+if (isNode) {
+ scheduleFlush = useNextTick();
+} else if (BrowserMutationObserver) {
+ scheduleFlush = useMutationObserver();
+} else if (isWorker) {
+ scheduleFlush = useMessageChannel();
+} else if (browserWindow === undefined && typeof require === 'function') {
+ scheduleFlush = attemptVertx();
+} else {
+ scheduleFlush = useSetTimeout();
+}
+
+function then(onFulfillment, onRejection) {
+ var _arguments = arguments;
+
+ var parent = this;
+
+ var child = new this.constructor(noop);
+
+ if (child[PROMISE_ID] === undefined) {
+ makePromise(child);
+ }
+
+ var _state = parent._state;
+
+ if (_state) {
+ (function () {
+ var callback = _arguments[_state - 1];
+ asap(function () {
+ return invokeCallback(_state, child, callback, parent._result);
+ });
+ })();
+ } else {
+ subscribe(parent, child, onFulfillment, onRejection);
+ }
+
+ return child;
+}
+
+/**
+ `Promise.resolve` returns a promise that will become resolved with the
+ passed `value`. It is shorthand for the following:
+
+ ```javascript
+ let promise = new Promise(function(resolve, reject){
+ resolve(1);
+ });
+
+ promise.then(function(value){
+ // value === 1
+ });
+ ```
+
+ Instead of writing the above, your code now simply becomes the following:
+
+ ```javascript
+ let promise = Promise.resolve(1);
+
+ promise.then(function(value){
+ // value === 1
+ });
+ ```
+
+ @method resolve
+ @static
+ @param {Any} value value that the returned promise will be resolved with
+ Useful for tooling.
+ @return {Promise} a promise that will become fulfilled with the given
+ `value`
+*/
+function resolve(object) {
+ /*jshint validthis:true */
+ var Constructor = this;
+
+ if (object && typeof object === 'object' && object.constructor === Constructor) {
+ return object;
+ }
+
+ var promise = new Constructor(noop);
+ _resolve(promise, object);
+ return promise;
+}
+
+var PROMISE_ID = Math.random().toString(36).substring(16);
+
+function noop() {}
+
+var PENDING = void 0;
+var FULFILLED = 1;
+var REJECTED = 2;
+
+var GET_THEN_ERROR = new ErrorObject();
+
+function selfFulfillment() {
+ return new TypeError("You cannot resolve a promise with itself");
+}
+
+function cannotReturnOwn() {
+ return new TypeError('A promises callback cannot return that same promise.');
+}
+
+function getThen(promise) {
+ try {
+ return promise.then;
+ } catch (error) {
+ GET_THEN_ERROR.error = error;
+ return GET_THEN_ERROR;
+ }
+}
+
+function tryThen(then, value, fulfillmentHandler, rejectionHandler) {
+ try {
+ then.call(value, fulfillmentHandler, rejectionHandler);
+ } catch (e) {
+ return e;
+ }
+}
+
+function handleForeignThenable(promise, thenable, then) {
+ asap(function (promise) {
+ var sealed = false;
+ var error = tryThen(then, thenable, function (value) {
+ if (sealed) {
+ return;
+ }
+ sealed = true;
+ if (thenable !== value) {
+ _resolve(promise, value);
+ } else {
+ fulfill(promise, value);
+ }
+ }, function (reason) {
+ if (sealed) {
+ return;
+ }
+ sealed = true;
+
+ _reject(promise, reason);
+ }, 'Settle: ' + (promise._label || ' unknown promise'));
+
+ if (!sealed && error) {
+ sealed = true;
+ _reject(promise, error);
+ }
+ }, promise);
+}
+
+function handleOwnThenable(promise, thenable) {
+ if (thenable._state === FULFILLED) {
+ fulfill(promise, thenable._result);
+ } else if (thenable._state === REJECTED) {
+ _reject(promise, thenable._result);
+ } else {
+ subscribe(thenable, undefined, function (value) {
+ return _resolve(promise, value);
+ }, function (reason) {
+ return _reject(promise, reason);
+ });
+ }
+}
+
+function handleMaybeThenable(promise, maybeThenable, then$$) {
+ if (maybeThenable.constructor === promise.constructor && then$$ === then && maybeThenable.constructor.resolve === resolve) {
+ handleOwnThenable(promise, maybeThenable);
+ } else {
+ if (then$$ === GET_THEN_ERROR) {
+ _reject(promise, GET_THEN_ERROR.error);
+ } else if (then$$ === undefined) {
+ fulfill(promise, maybeThenable);
+ } else if (isFunction(then$$)) {
+ handleForeignThenable(promise, maybeThenable, then$$);
+ } else {
+ fulfill(promise, maybeThenable);
+ }
+ }
+}
+
+function _resolve(promise, value) {
+ if (promise === value) {
+ _reject(promise, selfFulfillment());
+ } else if (objectOrFunction(value)) {
+ handleMaybeThenable(promise, value, getThen(value));
+ } else {
+ fulfill(promise, value);
+ }
+}
+
+function publishRejection(promise) {
+ if (promise._onerror) {
+ promise._onerror(promise._result);
+ }
+
+ publish(promise);
+}
+
+function fulfill(promise, value) {
+ if (promise._state !== PENDING) {
+ return;
+ }
+
+ promise._result = value;
+ promise._state = FULFILLED;
+
+ if (promise._subscribers.length !== 0) {
+ asap(publish, promise);
+ }
+}
+
+function _reject(promise, reason) {
+ if (promise._state !== PENDING) {
+ return;
+ }
+ promise._state = REJECTED;
+ promise._result = reason;
+
+ asap(publishRejection, promise);
+}
+
+function subscribe(parent, child, onFulfillment, onRejection) {
+ var _subscribers = parent._subscribers;
+ var length = _subscribers.length;
+
+ parent._onerror = null;
+
+ _subscribers[length] = child;
+ _subscribers[length + FULFILLED] = onFulfillment;
+ _subscribers[length + REJECTED] = onRejection;
+
+ if (length === 0 && parent._state) {
+ asap(publish, parent);
+ }
+}
+
+function publish(promise) {
+ var subscribers = promise._subscribers;
+ var settled = promise._state;
+
+ if (subscribers.length === 0) {
+ return;
+ }
+
+ var child = undefined,
+ callback = undefined,
+ detail = promise._result;
+
+ for (var i = 0; i < subscribers.length; i += 3) {
+ child = subscribers[i];
+ callback = subscribers[i + settled];
+
+ if (child) {
+ invokeCallback(settled, child, callback, detail);
+ } else {
+ callback(detail);
+ }
+ }
+
+ promise._subscribers.length = 0;
+}
+
+function ErrorObject() {
+ this.error = null;
+}
+
+var TRY_CATCH_ERROR = new ErrorObject();
+
+function tryCatch(callback, detail) {
+ try {
+ return callback(detail);
+ } catch (e) {
+ TRY_CATCH_ERROR.error = e;
+ return TRY_CATCH_ERROR;
+ }
+}
+
+function invokeCallback(settled, promise, callback, detail) {
+ var hasCallback = isFunction(callback),
+ value = undefined,
+ error = undefined,
+ succeeded = undefined,
+ failed = undefined;
+
+ if (hasCallback) {
+ value = tryCatch(callback, detail);
+
+ if (value === TRY_CATCH_ERROR) {
+ failed = true;
+ error = value.error;
+ value = null;
+ } else {
+ succeeded = true;
+ }
+
+ if (promise === value) {
+ _reject(promise, cannotReturnOwn());
+ return;
+ }
+ } else {
+ value = detail;
+ succeeded = true;
+ }
+
+ if (promise._state !== PENDING) {
+ // noop
+ } else if (hasCallback && succeeded) {
+ _resolve(promise, value);
+ } else if (failed) {
+ _reject(promise, error);
+ } else if (settled === FULFILLED) {
+ fulfill(promise, value);
+ } else if (settled === REJECTED) {
+ _reject(promise, value);
+ }
+}
+
+function initializePromise(promise, resolver) {
+ try {
+ resolver(function resolvePromise(value) {
+ _resolve(promise, value);
+ }, function rejectPromise(reason) {
+ _reject(promise, reason);
+ });
+ } catch (e) {
+ _reject(promise, e);
+ }
+}
+
+var id = 0;
+function nextId() {
+ return id++;
+}
+
+function makePromise(promise) {
+ promise[PROMISE_ID] = id++;
+ promise._state = undefined;
+ promise._result = undefined;
+ promise._subscribers = [];
+}
+
+function Enumerator(Constructor, input) {
+ this._instanceConstructor = Constructor;
+ this.promise = new Constructor(noop);
+
+ if (!this.promise[PROMISE_ID]) {
+ makePromise(this.promise);
+ }
+
+ if (isArray(input)) {
+ this._input = input;
+ this.length = input.length;
+ this._remaining = input.length;
+
+ this._result = new Array(this.length);
+
+ if (this.length === 0) {
+ fulfill(this.promise, this._result);
+ } else {
+ this.length = this.length || 0;
+ this._enumerate();
+ if (this._remaining === 0) {
+ fulfill(this.promise, this._result);
+ }
+ }
+ } else {
+ _reject(this.promise, validationError());
+ }
+}
+
+function validationError() {
+ return new Error('Array Methods must be provided an Array');
+};
+
+Enumerator.prototype._enumerate = function () {
+ var length = this.length;
+ var _input = this._input;
+
+ for (var i = 0; this._state === PENDING && i < length; i++) {
+ this._eachEntry(_input[i], i);
+ }
+};
+
+Enumerator.prototype._eachEntry = function (entry, i) {
+ var c = this._instanceConstructor;
+ var resolve$$ = c.resolve;
+
+ if (resolve$$ === resolve) {
+ var _then = getThen(entry);
+
+ if (_then === then && entry._state !== PENDING) {
+ this._settledAt(entry._state, i, entry._result);
+ } else if (typeof _then !== 'function') {
+ this._remaining--;
+ this._result[i] = entry;
+ } else if (c === Promise) {
+ var promise = new c(noop);
+ handleMaybeThenable(promise, entry, _then);
+ this._willSettleAt(promise, i);
+ } else {
+ this._willSettleAt(new c(function (resolve$$) {
+ return resolve$$(entry);
+ }), i);
+ }
+ } else {
+ this._willSettleAt(resolve$$(entry), i);
+ }
+};
+
+Enumerator.prototype._settledAt = function (state, i, value) {
+ var promise = this.promise;
+
+ if (promise._state === PENDING) {
+ this._remaining--;
+
+ if (state === REJECTED) {
+ _reject(promise, value);
+ } else {
+ this._result[i] = value;
+ }
+ }
+
+ if (this._remaining === 0) {
+ fulfill(promise, this._result);
+ }
+};
+
+Enumerator.prototype._willSettleAt = function (promise, i) {
+ var enumerator = this;
+
+ subscribe(promise, undefined, function (value) {
+ return enumerator._settledAt(FULFILLED, i, value);
+ }, function (reason) {
+ return enumerator._settledAt(REJECTED, i, reason);
+ });
+};
+
+/**
+ `Promise.all` accepts an array of promises, and returns a new promise which
+ is fulfilled with an array of fulfillment values for the passed promises, or
+ rejected with the reason of the first passed promise to be rejected. It casts all
+ elements of the passed iterable to promises as it runs this algorithm.
+
+ Example:
+
+ ```javascript
+ let promise1 = resolve(1);
+ let promise2 = resolve(2);
+ let promise3 = resolve(3);
+ let promises = [ promise1, promise2, promise3 ];
+
+ Promise.all(promises).then(function(array){
+ // The array here would be [ 1, 2, 3 ];
+ });
+ ```
+
+ If any of the `promises` given to `all` are rejected, the first promise
+ that is rejected will be given as an argument to the returned promises's
+ rejection handler. For example:
+
+ Example:
+
+ ```javascript
+ let promise1 = resolve(1);
+ let promise2 = reject(new Error("2"));
+ let promise3 = reject(new Error("3"));
+ let promises = [ promise1, promise2, promise3 ];
+
+ Promise.all(promises).then(function(array){
+ // Code here never runs because there are rejected promises!
+ }, function(error) {
+ // error.message === "2"
+ });
+ ```
+
+ @method all
+ @static
+ @param {Array} entries array of promises
+ @param {String} label optional string for labeling the promise.
+ Useful for tooling.
+ @return {Promise} promise that is fulfilled when all `promises` have been
+ fulfilled, or rejected if any of them become rejected.
+ @static
+*/
+function all(entries) {
+ return new Enumerator(this, entries).promise;
+}
+
+/**
+ `Promise.race` returns a new promise which is settled in the same way as the
+ first passed promise to settle.
+
+ Example:
+
+ ```javascript
+ let promise1 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ resolve('promise 1');
+ }, 200);
+ });
+
+ let promise2 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ resolve('promise 2');
+ }, 100);
+ });
+
+ Promise.race([promise1, promise2]).then(function(result){
+ // result === 'promise 2' because it was resolved before promise1
+ // was resolved.
+ });
+ ```
+
+ `Promise.race` is deterministic in that only the state of the first
+ settled promise matters. For example, even if other promises given to the
+ `promises` array argument are resolved, but the first settled promise has
+ become rejected before the other promises became fulfilled, the returned
+ promise will become rejected:
+
+ ```javascript
+ let promise1 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ resolve('promise 1');
+ }, 200);
+ });
+
+ let promise2 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ reject(new Error('promise 2'));
+ }, 100);
+ });
+
+ Promise.race([promise1, promise2]).then(function(result){
+ // Code here never runs
+ }, function(reason){
+ // reason.message === 'promise 2' because promise 2 became rejected before
+ // promise 1 became fulfilled
+ });
+ ```
+
+ An example real-world use case is implementing timeouts:
+
+ ```javascript
+ Promise.race([ajax('foo.json'), timeout(5000)])
+ ```
+
+ @method race
+ @static
+ @param {Array} promises array of promises to observe
+ Useful for tooling.
+ @return {Promise} a promise which settles in the same way as the first passed
+ promise to settle.
+*/
+function race(entries) {
+ /*jshint validthis:true */
+ var Constructor = this;
+
+ if (!isArray(entries)) {
+ return new Constructor(function (_, reject) {
+ return reject(new TypeError('You must pass an array to race.'));
+ });
+ } else {
+ return new Constructor(function (resolve, reject) {
+ var length = entries.length;
+ for (var i = 0; i < length; i++) {
+ Constructor.resolve(entries[i]).then(resolve, reject);
+ }
+ });
+ }
+}
+
+/**
+ `Promise.reject` returns a promise rejected with the passed `reason`.
+ It is shorthand for the following:
+
+ ```javascript
+ let promise = new Promise(function(resolve, reject){
+ reject(new Error('WHOOPS'));
+ });
+
+ promise.then(function(value){
+ // Code here doesn't run because the promise is rejected!
+ }, function(reason){
+ // reason.message === 'WHOOPS'
+ });
+ ```
+
+ Instead of writing the above, your code now simply becomes the following:
+
+ ```javascript
+ let promise = Promise.reject(new Error('WHOOPS'));
+
+ promise.then(function(value){
+ // Code here doesn't run because the promise is rejected!
+ }, function(reason){
+ // reason.message === 'WHOOPS'
+ });
+ ```
+
+ @method reject
+ @static
+ @param {Any} reason value that the returned promise will be rejected with.
+ Useful for tooling.
+ @return {Promise} a promise rejected with the given `reason`.
+*/
+function reject(reason) {
+ /*jshint validthis:true */
+ var Constructor = this;
+ var promise = new Constructor(noop);
+ _reject(promise, reason);
+ return promise;
+}
+
+function needsResolver() {
+ throw new TypeError('You must pass a resolver function as the first argument to the promise constructor');
+}
+
+function needsNew() {
+ throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");
+}
+
+/**
+ Promise objects represent the eventual result of an asynchronous operation. The
+ primary way of interacting with a promise is through its `then` method, which
+ registers callbacks to receive either a promise's eventual value or the reason
+ why the promise cannot be fulfilled.
+
+ Terminology
+ -----------
+
+ - `promise` is an object or function with a `then` method whose behavior conforms to this specification.
+ - `thenable` is an object or function that defines a `then` method.
+ - `value` is any legal JavaScript value (including undefined, a thenable, or a promise).
+ - `exception` is a value that is thrown using the throw statement.
+ - `reason` is a value that indicates why a promise was rejected.
+ - `settled` the final resting state of a promise, fulfilled or rejected.
+
+ A promise can be in one of three states: pending, fulfilled, or rejected.
+
+ Promises that are fulfilled have a fulfillment value and are in the fulfilled
+ state. Promises that are rejected have a rejection reason and are in the
+ rejected state. A fulfillment value is never a thenable.
+
+ Promises can also be said to *resolve* a value. If this value is also a
+ promise, then the original promise's settled state will match the value's
+ settled state. So a promise that *resolves* a promise that rejects will
+ itself reject, and a promise that *resolves* a promise that fulfills will
+ itself fulfill.
+
+
+ Basic Usage:
+ ------------
+
+ ```js
+ let promise = new Promise(function(resolve, reject) {
+ // on success
+ resolve(value);
+
+ // on failure
+ reject(reason);
+ });
+
+ promise.then(function(value) {
+ // on fulfillment
+ }, function(reason) {
+ // on rejection
+ });
+ ```
+
+ Advanced Usage:
+ ---------------
+
+ Promises shine when abstracting away asynchronous interactions such as
+ `XMLHttpRequest`s.
+
+ ```js
+ function getJSON(url) {
+ return new Promise(function(resolve, reject){
+ let xhr = new XMLHttpRequest();
+
+ xhr.open('GET', url);
+ xhr.onreadystatechange = handler;
+ xhr.responseType = 'json';
+ xhr.setRequestHeader('Accept', 'application/json');
+ xhr.send();
+
+ function handler() {
+ if (this.readyState === this.DONE) {
+ if (this.status === 200) {
+ resolve(this.response);
+ } else {
+ reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']'));
+ }
+ }
+ };
+ });
+ }
+
+ getJSON('/posts.json').then(function(json) {
+ // on fulfillment
+ }, function(reason) {
+ // on rejection
+ });
+ ```
+
+ Unlike callbacks, promises are great composable primitives.
+
+ ```js
+ Promise.all([
+ getJSON('/posts'),
+ getJSON('/comments')
+ ]).then(function(values){
+ values[0] // => postsJSON
+ values[1] // => commentsJSON
+
+ return values;
+ });
+ ```
+
+ @class Promise
+ @param {function} resolver
+ Useful for tooling.
+ @constructor
+*/
+function Promise(resolver) {
+ this[PROMISE_ID] = nextId();
+ this._result = this._state = undefined;
+ this._subscribers = [];
+
+ if (noop !== resolver) {
+ typeof resolver !== 'function' && needsResolver();
+ this instanceof Promise ? initializePromise(this, resolver) : needsNew();
+ }
+}
+
+Promise.all = all;
+Promise.race = race;
+Promise.resolve = resolve;
+Promise.reject = reject;
+Promise._setScheduler = setScheduler;
+Promise._setAsap = setAsap;
+Promise._asap = asap;
+
+Promise.prototype = {
+ constructor: Promise,
+
+ /**
+ The primary way of interacting with a promise is through its `then` method,
+ which registers callbacks to receive either a promise's eventual value or the
+ reason why the promise cannot be fulfilled.
+
+ ```js
+ findUser().then(function(user){
+ // user is available
+ }, function(reason){
+ // user is unavailable, and you are given the reason why
+ });
+ ```
+
+ Chaining
+ --------
+
+ The return value of `then` is itself a promise. This second, 'downstream'
+ promise is resolved with the return value of the first promise's fulfillment
+ or rejection handler, or rejected if the handler throws an exception.
+
+ ```js
+ findUser().then(function (user) {
+ return user.name;
+ }, function (reason) {
+ return 'default name';
+ }).then(function (userName) {
+ // If `findUser` fulfilled, `userName` will be the user's name, otherwise it
+ // will be `'default name'`
+ });
+
+ findUser().then(function (user) {
+ throw new Error('Found user, but still unhappy');
+ }, function (reason) {
+ throw new Error('`findUser` rejected and we're unhappy');
+ }).then(function (value) {
+ // never reached
+ }, function (reason) {
+ // if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'.
+ // If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'.
+ });
+ ```
+ If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream.
+
+ ```js
+ findUser().then(function (user) {
+ throw new PedagogicalException('Upstream error');
+ }).then(function (value) {
+ // never reached
+ }).then(function (value) {
+ // never reached
+ }, function (reason) {
+ // The `PedgagocialException` is propagated all the way down to here
+ });
+ ```
+
+ Assimilation
+ ------------
+
+ Sometimes the value you want to propagate to a downstream promise can only be
+ retrieved asynchronously. This can be achieved by returning a promise in the
+ fulfillment or rejection handler. The downstream promise will then be pending
+ until the returned promise is settled. This is called *assimilation*.
+
+ ```js
+ findUser().then(function (user) {
+ return findCommentsByAuthor(user);
+ }).then(function (comments) {
+ // The user's comments are now available
+ });
+ ```
+
+ If the assimliated promise rejects, then the downstream promise will also reject.
+
+ ```js
+ findUser().then(function (user) {
+ return findCommentsByAuthor(user);
+ }).then(function (comments) {
+ // If `findCommentsByAuthor` fulfills, we'll have the value here
+ }, function (reason) {
+ // If `findCommentsByAuthor` rejects, we'll have the reason here
+ });
+ ```
+
+ Simple Example
+ --------------
+
+ Synchronous Example
+
+ ```javascript
+ let result;
+
+ try {
+ result = findResult();
+ // success
+ } catch(reason) {
+ // failure
+ }
+ ```
+
+ Errback Example
+
+ ```js
+ findResult(function(result, err){
+ if (err) {
+ // failure
+ } else {
+ // success
+ }
+ });
+ ```
+
+ Promise Example;
+
+ ```javascript
+ findResult().then(function(result){
+ // success
+ }, function(reason){
+ // failure
+ });
+ ```
+
+ Advanced Example
+ --------------
+
+ Synchronous Example
+
+ ```javascript
+ let author, books;
+
+ try {
+ author = findAuthor();
+ books = findBooksByAuthor(author);
+ // success
+ } catch(reason) {
+ // failure
+ }
+ ```
+
+ Errback Example
+
+ ```js
+
+ function foundBooks(books) {
+
+ }
+
+ function failure(reason) {
+
+ }
+
+ findAuthor(function(author, err){
+ if (err) {
+ failure(err);
+ // failure
+ } else {
+ try {
+ findBoooksByAuthor(author, function(books, err) {
+ if (err) {
+ failure(err);
+ } else {
+ try {
+ foundBooks(books);
+ } catch(reason) {
+ failure(reason);
+ }
+ }
+ });
+ } catch(error) {
+ failure(err);
+ }
+ // success
+ }
+ });
+ ```
+
+ Promise Example;
+
+ ```javascript
+ findAuthor().
+ then(findBooksByAuthor).
+ then(function(books){
+ // found books
+ }).catch(function(reason){
+ // something went wrong
+ });
+ ```
+
+ @method then
+ @param {Function} onFulfilled
+ @param {Function} onRejected
+ Useful for tooling.
+ @return {Promise}
+ */
+ then: then,
+
+ /**
+ `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same
+ as the catch block of a try/catch statement.
+
+ ```js
+ function findAuthor(){
+ throw new Error('couldn't find that author');
+ }
+
+ // synchronous
+ try {
+ findAuthor();
+ } catch(reason) {
+ // something went wrong
+ }
+
+ // async with promises
+ findAuthor().catch(function(reason){
+ // something went wrong
+ });
+ ```
+
+ @method catch
+ @param {Function} onRejection
+ Useful for tooling.
+ @return {Promise}
+ */
+ 'catch': function _catch(onRejection) {
+ return this.then(null, onRejection);
+ }
+};
+
+function polyfill() {
+ var local = undefined;
+
+ if (typeof global !== 'undefined') {
+ local = global;
+ } else if (typeof self !== 'undefined') {
+ local = self;
+ } else {
+ try {
+ local = Function('return this')();
+ } catch (e) {
+ throw new Error('polyfill failed because global object is unavailable in this environment');
+ }
+ }
+
+ var P = local.Promise;
+
+ if (P) {
+ var promiseToString = null;
+ try {
+ promiseToString = Object.prototype.toString.call(P.resolve());
+ } catch (e) {
+ // silently ignored
+ }
+
+ if (promiseToString === '[object Promise]' && !P.cast) {
+ return;
+ }
+ }
+
+ local.Promise = Promise;
+}
+
+// Strange compat..
+Promise.polyfill = polyfill;
+Promise.Promise = Promise;
+
+return Promise;
+
+})));
+
+ES6Promise.polyfill();
+//# sourceMappingURL=es6-promise.auto.map