diff options
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? = 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 Binary files differnew file mode 100644 index 00000000000..18839d447e8 --- /dev/null +++ b/doc/development/ux_guide/img/copy-form-addissuebutton.png diff --git a/doc/development/ux_guide/img/copy-form-addissueform.png b/doc/development/ux_guide/img/copy-form-addissueform.png Binary files differnew file mode 100644 index 00000000000..e6838c06eca --- /dev/null +++ b/doc/development/ux_guide/img/copy-form-addissueform.png diff --git a/doc/development/ux_guide/img/copy-form-editissuebutton.png b/doc/development/ux_guide/img/copy-form-editissuebutton.png Binary files differnew file mode 100644 index 00000000000..2435820e14f --- /dev/null +++ b/doc/development/ux_guide/img/copy-form-editissuebutton.png diff --git a/doc/development/ux_guide/img/copy-form-editissueform.png b/doc/development/ux_guide/img/copy-form-editissueform.png Binary files differnew file mode 100644 index 00000000000..5ddeda33e68 --- /dev/null +++ b/doc/development/ux_guide/img/copy-form-editissueform.png 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 |