diff options
176 files changed, 4118 insertions, 2788 deletions
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 01c96e06547..2d06a8acc58 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -139,7 +139,7 @@ setup-test-env: rspec unit pg: <<: *rspec-metadata-pg - parallel: 25 + parallel: 20 rspec integration pg: <<: *rspec-metadata-pg @@ -152,7 +152,7 @@ rspec system pg: rspec unit pg-10: <<: *rspec-metadata-pg-10 <<: *only-schedules-master - parallel: 25 + parallel: 20 rspec integration pg-10: <<: *rspec-metadata-pg-10 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 698570efb07..41714eefa97 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -466,12 +466,10 @@ Rails/LinkToBlank: Rails/Presence: Exclude: - 'app/models/ci/pipeline.rb' - - 'app/models/clusters/platforms/kubernetes.rb' - 'app/models/concerns/mentionable.rb' - 'app/models/project_services/hipchat_service.rb' - 'app/models/project_services/irker_service.rb' - 'app/models/project_services/jira_service.rb' - - 'app/models/project_services/kubernetes_service.rb' - 'app/models/project_services/packagist_service.rb' - 'app/models/wiki_page.rb' - 'lib/gitlab/github_import/importer/releases_importer.rb' @@ -514,7 +512,6 @@ Security/YAMLLoad: - 'spec/config/mail_room_spec.rb' - 'spec/initializers/secret_token_spec.rb' - 'spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb' - - 'spec/models/project_services/kubernetes_service_spec.rb' # Offense count: 34 # Configuration parameters: EnforcedStyle. @@ -132,7 +132,7 @@ gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 2.0.10' gem 'asciidoctor-include-ext', '~> 0.3.1', require: false gem 'asciidoctor-plantuml', '0.0.9' -gem 'rouge', '~> 3.1' +gem 'rouge', '~> 3.5' gem 'truncato', '~> 0.7.11' gem 'bootstrap_form', '~> 4.2.0' gem 'nokogiri', '~> 1.10.3' @@ -429,7 +429,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 1.32.0', require: 'gitaly' +gem 'gitaly-proto', '~> 1.36.0', require: 'gitaly' gem 'grpc', '~> 1.19.0' diff --git a/Gemfile.lock b/Gemfile.lock index 5b648d43137..363baa5f9d5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -303,7 +303,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (1.32.0) + gitaly-proto (1.36.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-labkit (0.3.0) @@ -770,7 +770,7 @@ GEM retriable (3.1.2) rinku (2.0.0) rotp (2.1.2) - rouge (3.4.1) + rouge (3.5.1) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -1092,7 +1092,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 1.32.0) + gitaly-proto (~> 1.36.0) github-markup (~> 1.7.0) gitlab-labkit (~> 0.3.0) gitlab-markup (~> 1.7.0) @@ -1199,7 +1199,7 @@ DEPENDENCIES redis-rails (~> 5.0.2) request_store (~> 1.3) responders (~> 2.0) - rouge (~> 3.1) + rouge (~> 3.5) rqrcode-rails3 (~> 0.1.7) rspec-parameterized rspec-rails (~> 3.7.0) diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js index 96bc6a5f8e8..7dbaf984acf 100644 --- a/app/assets/javascripts/branches/divergence_graph.js +++ b/app/assets/javascripts/branches/divergence_graph.js @@ -36,7 +36,9 @@ export default endpoint => { }, 100); Object.entries(data).forEach(([branchName, val]) => { - const el = document.querySelector(`.js-branch-${branchName} .js-branch-divergence-graph`); + const el = document.querySelector( + `[data-name="${branchName}"] .js-branch-divergence-graph`, + ); if (!el) return; diff --git a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue new file mode 100644 index 00000000000..444640980af --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue @@ -0,0 +1,58 @@ +<script> +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { __ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + Icon, + }, + props: { + projects: { + type: Array, + required: true, + }, + selectedProject: { + type: Object, + required: false, + default: () => ({}), + }, + }, + computed: { + dropdownText() { + if (Object.keys(this.selectedProject).length) { + return this.selectedProject.name; + } + + return __('Select private project'); + }, + }, + methods: { + selectProject(project) { + this.$emit('click', project); + }, + }, +}; +</script> + +<template> + <gl-dropdown toggle-class="d-flex align-items-center w-100" class="w-100"> + <template slot="button-content"> + <span class="str-truncated-100 mr-2"> + <icon name="lock" /> + {{ dropdownText }} + </span> + <icon name="chevron-down" class="ml-auto" /> + </template> + <gl-dropdown-item v-for="project in projects" :key="project.id" @click="selectProject(project)"> + <icon + name="mobile-issue-close" + :class="{ icon: project.id !== selectedProject.id }" + class="js-active-project-check" + /> + <span class="ml-1">{{ project.name }}</span> + </gl-dropdown-item> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue new file mode 100644 index 00000000000..b89729375be --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue @@ -0,0 +1,136 @@ +<script> +import { GlLink } from '@gitlab/ui'; +import { __, sprintf } from '../../locale'; +import createFlash from '../../flash'; +import Api from '../../api'; +import state from '../state'; +import Dropdown from './dropdown.vue'; + +export default { + components: { + GlLink, + Dropdown, + }, + props: { + namespacePath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + newForkPath: { + type: String, + required: true, + }, + helpPagePath: { + type: String, + required: true, + }, + }, + data() { + return { + projects: [], + }; + }, + computed: { + selectedProject() { + return state.selectedProject; + }, + noForkText() { + return sprintf( + __( + 'To protect this issues confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private.', + ), + { link_start: `<a href="${this.newForkPath}" class="help-link">`, link_end: '</a>' }, + false, + ); + }, + }, + mounted() { + this.fetchProjects(); + this.createBtn = document.querySelector('.js-create-target'); + this.warningText = document.querySelector('.js-exposed-info-warning'); + }, + methods: { + selectProject(project) { + if (project) { + Object.assign(state, { + selectedProject: project, + }); + + if (project.namespaceFullPath !== this.namespacePath) { + this.showWarning(); + } + } else if (this.createBtn) { + this.createBtn.setAttribute('disabled', 'disabled'); + } + }, + normalizeProjectData(data) { + return data.map(p => ({ + id: p.id, + name: p.name_with_namespace, + pathWithNamespace: p.path_with_namespace, + namespaceFullpath: p.namespace.full_path, + })); + }, + fetchProjects() { + Api.projectForks(this.projectPath, { + with_merge_requests_enabled: true, + min_access_level: 30, + visibility: 'private', + }) + .then(({ data }) => { + this.projects = this.normalizeProjectData(data); + this.selectProject(this.projects[0]); + }) + .catch(e => { + createFlash(__('Error fetching forked projects. Please try again.')); + throw e; + }); + }, + showWarning() { + if (this.warningText) { + this.warningText.classList.remove('hidden'); + } + + if (this.createBtn) { + this.createBtn.classList.add('btn-warning'); + this.createBtn.classList.remove('btn-success'); + } + }, + }, +}; +</script> + +<template> + <div class="form-group"> + <label>{{ __('Project') }}</label> + <div> + <dropdown + v-if="projects.length" + :projects="projects" + :selected-project="selectedProject" + @click="selectProject" + /> + <p class="text-muted mt-1 mb-0"> + <template v-if="projects.length"> + {{ + __( + 'To protect this issues confidentiality, a private fork of this project was selected.', + ) + }} + </template> + <template v-else> + {{ __('No forks available to you.') }}<br /> + <span v-html="noForkText"></span> + </template> + <gl-link :href="helpPagePath" class="help-link" target="_blank"> + <span class="sr-only">{{ __('Read more') }}</span> + <i class="fa fa-question-circle" aria-hidden="true"></i> + </gl-link> + </p> + </div> + </div> +</template> diff --git a/app/assets/javascripts/confidential_merge_request/index.js b/app/assets/javascripts/confidential_merge_request/index.js new file mode 100644 index 00000000000..9672821d30e --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/index.js @@ -0,0 +1,30 @@ +import Vue from 'vue'; +import { parseBoolean } from '../lib/utils/common_utils'; +import ProjectFormGroup from './components/project_form_group.vue'; +import state from './state'; + +export function isConfidentialIssue() { + return parseBoolean(document.querySelector('.js-create-mr').dataset.isConfidential); +} + +export function canCreateConfidentialMergeRequest() { + return isConfidentialIssue() && Object.keys(state.selectedProject).length > 0; +} + +export function init() { + const el = document.getElementById('js-forked-project'); + + return new Vue({ + el, + render(h) { + return h(ProjectFormGroup, { + props: { + namespacePath: el.dataset.namespacePath, + projectPath: el.dataset.projectPath, + newForkPath: el.dataset.newForkPath, + helpPagePath: el.dataset.helpPagePath, + }, + }); + }, + }); +} diff --git a/app/assets/javascripts/confidential_merge_request/state.js b/app/assets/javascripts/confidential_merge_request/state.js new file mode 100644 index 00000000000..95b0580f4b9 --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/state.js @@ -0,0 +1,5 @@ +import Vue from 'vue'; + +export default Vue.observable({ + selectedProject: {}, +}); diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index 8f5cece0788..052168bb21c 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -5,6 +5,12 @@ import Flash from './flash'; import DropLab from './droplab/drop_lab'; import ISetter from './droplab/plugins/input_setter'; import { __, sprintf } from './locale'; +import { + init as initConfidentialMergeRequest, + isConfidentialIssue, + canCreateConfidentialMergeRequest, +} from './confidential_merge_request'; +import confidentialMergeRequestState from './confidential_merge_request/state'; // Todo: Remove this when fixing issue in input_setter plugin const InputSetter = Object.assign({}, ISetter); @@ -12,6 +18,17 @@ const InputSetter = Object.assign({}, ISetter); const CREATE_MERGE_REQUEST = 'create-mr'; const CREATE_BRANCH = 'create-branch'; +function createEndpoint(projectPath, endpoint) { + if (canCreateConfidentialMergeRequest()) { + return endpoint.replace( + projectPath, + confidentialMergeRequestState.selectedProject.pathWithNamespace, + ); + } + + return endpoint; +} + export default class CreateMergeRequestDropdown { constructor(wrapperEl) { this.wrapperEl = wrapperEl; @@ -42,6 +59,8 @@ export default class CreateMergeRequestDropdown { this.refIsValid = true; this.refsPath = this.wrapperEl.dataset.refsPath; this.suggestedRef = this.refInput.value; + this.projectPath = this.wrapperEl.dataset.projectPath; + this.projectId = this.wrapperEl.dataset.projectId; // These regexps are used to replace // a backend generated new branch name and its source (ref) @@ -58,6 +77,14 @@ export default class CreateMergeRequestDropdown { }; this.init(); + + if (isConfidentialIssue()) { + this.createMergeRequestButton.setAttribute( + 'data-dropdown-trigger', + '#create-merge-request-dropdown', + ); + initConfidentialMergeRequest(); + } } available() { @@ -113,7 +140,9 @@ export default class CreateMergeRequestDropdown { this.isCreatingBranch = true; return axios - .post(this.createBranchPath) + .post(createEndpoint(this.projectPath, this.createBranchPath), { + confidential_issue_project_id: canCreateConfidentialMergeRequest() ? this.projectId : null, + }) .then(({ data }) => { this.branchCreated = true; window.location.href = data.url; @@ -125,7 +154,11 @@ export default class CreateMergeRequestDropdown { this.isCreatingMergeRequest = true; return axios - .post(this.createMrPath) + .post(this.createMrPath, { + target_project_id: canCreateConfidentialMergeRequest() + ? confidentialMergeRequestState.selectedProject.id + : null, + }) .then(({ data }) => { this.mergeRequestCreated = true; window.location.href = data.url; @@ -149,6 +182,8 @@ export default class CreateMergeRequestDropdown { } enable() { + if (!canCreateConfidentialMergeRequest()) return; + this.createMergeRequestButton.classList.remove('disabled'); this.createMergeRequestButton.removeAttribute('disabled'); @@ -205,7 +240,7 @@ export default class CreateMergeRequestDropdown { if (!ref) return false; return axios - .get(`${this.refsPath}${encodeURIComponent(ref)}`) + .get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`) .then(({ data }) => { const branches = data[Object.keys(data)[0]]; const tags = data[Object.keys(data)[1]]; @@ -325,6 +360,12 @@ export default class CreateMergeRequestDropdown { let xhr = null; event.preventDefault(); + if (isConfidentialIssue() && !event.target.classList.contains('js-create-target')) { + this.droplab.hooks.forEach(hook => hook.list.toggle()); + + return; + } + if (this.isBusy()) { return; } diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index 84e1f1c4c20..721942f9d3b 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -36,15 +36,26 @@ function removeTimeSeriesNoData(queries) { // { metricId: 2, ...query2Attrs }] }, // { title: 'new title', y_label: 'MB', queries: [{ metricId: 3, ...query3Attrs }]} // ] -function groupQueriesByChartInfo(metrics) { +export function groupQueriesByChartInfo(metrics) { const metricsByChart = metrics.reduce((accumulator, metric) => { const { queries, ...chart } = metric; - const metricId = chart.id ? chart.id.toString() : null; const chartKey = `${chart.title}|${chart.y_label}`; accumulator[chartKey] = accumulator[chartKey] || { ...chart, queries: [] }; - queries.forEach(queryAttrs => accumulator[chartKey].queries.push({ metricId, ...queryAttrs })); + queries.forEach(queryAttrs => { + let metricId; + + if (chart.id) { + metricId = chart.id.toString(); + } else if (queryAttrs.metric_id) { + metricId = queryAttrs.metric_id.toString(); + } else { + metricId = null; + } + + accumulator[chartKey].queries.push({ metricId, ...queryAttrs }); + }); return accumulator; }, {}); diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 3fbd0a9f715..ac743d9f4b8 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -144,15 +144,6 @@ export default { return {}; }, - componentClassName() { - if (this.shouldRenderDiffs) { - if (!this.lastUpdatedAt && !this.discussion.resolved) { - return 'unresolved'; - } - } - - return ''; - }, isExpanded() { return this.discussion.expanded || this.alwaysExpanded; }, @@ -313,11 +304,11 @@ export default { </script> <template> - <timeline-entry-item class="note note-discussion" :class="componentClassName"> + <timeline-entry-item class="note note-discussion"> <div class="timeline-content"> <div :data-discussion-id="discussion.id" class="discussion js-discussion-container"> <div v-if="shouldRenderDiffs" class="discussion-header note-wrapper"> - <div v-once class="timeline-icon"> + <div v-once class="timeline-icon align-self-start flex-shrink-0"> <user-avatar-link v-if="author" :link-href="author.path" @@ -326,7 +317,7 @@ export default { :img-size="40" /> </div> - <div class="timeline-content"> + <div class="timeline-content w-100"> <note-header :author="author" :created-at="firstNote.created_at" diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js index 237e70c0a4c..47a6f07cce2 100644 --- a/app/assets/javascripts/notes/services/notes_service.js +++ b/app/assets/javascripts/notes/services/notes_service.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import Api from '~/api'; import VueResource from 'vue-resource'; import * as constants from '../constants'; @@ -45,7 +44,4 @@ export default { toggleIssueState(endpoint, data) { return Vue.http.put(endpoint, data); }, - applySuggestion(id) { - return Api.applySuggestion(id); - }, }; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 9054b4779aa..fef962f008e 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -14,6 +14,7 @@ import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils'; import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub'; import { __ } from '~/locale'; +import Api from '~/api'; let eTagPoll; @@ -449,8 +450,7 @@ export const submitSuggestion = ( { commit, dispatch }, { discussionId, noteId, suggestionId, flashContainer }, ) => - service - .applySuggestion(suggestionId) + Api.applySuggestion(suggestionId) .then(() => commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId })) .then(() => dispatch('resolveDiscussion', { discussionId }).catch(() => {})) .catch(err => { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cd951f67293..a12029d2419 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -287,8 +287,8 @@ list-style: none; padding: 0 1px; - a, - button, + a:not(.help-link), + button:not(.btn), .menu-item { @include dropdown-link; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 7bd1a4138e4..5db0136e3f1 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -406,7 +406,7 @@ $note-form-margin-left: 72px; border-radius: 0; @media (min-width: map-get($grid-breakpoints, md)) { - top: 91px; + top: $mr-tabs-height + $header-height; .with-performance-bar & { top: 126px; @@ -598,7 +598,8 @@ $note-form-margin-left: 72px; } .discussion-header { - min-height: 74px; + min-height: $line-height-base * 2em; + box-sizing: content-box; .note-header-info { padding-bottom: 0; @@ -608,13 +609,10 @@ $note-form-margin-left: 72px; overflow-x: auto; overflow-y: hidden; } -} -.unresolved { - .discussion-header { - .note-header-info { - margin-top: $gl-padding-8; - } + &.note-wrapper { + display: flex; + align-items: center; } } diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index d77f64a84f5..141a7dfb923 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -169,7 +169,7 @@ class Projects::BranchesController < Projects::ApplicationController end def confidential_issue_project - return unless Feature.enabled?(:create_confidential_merge_request, @project) + return unless helpers.create_confidential_merge_request_enabled? return if params[:confidential_issue_project_id].blank? confidential_issue_project = Project.find(params[:confidential_issue_project_id]) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index e275b417784..b866f574f67 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -172,7 +172,7 @@ class Projects::IssuesController < Projects::ApplicationController def create_merge_request create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid) - create_params[:target_project_id] = params[:target_project_id] if Feature.enabled?(:create_confidential_merge_request, @project) + create_params[:target_project_id] = params[:target_project_id] if helpers.create_confidential_merge_request_enabled? result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute if result[:status] == :success diff --git a/app/finders/runner_jobs_finder.rb b/app/finders/runner_jobs_finder.rb index 4fca4ec94f3..ef90817416a 100644 --- a/app/finders/runner_jobs_finder.rb +++ b/app/finders/runner_jobs_finder.rb @@ -3,6 +3,8 @@ class RunnerJobsFinder attr_reader :runner, :params + ALLOWED_INDEXED_COLUMNS = %w[id].freeze + def initialize(runner, params = {}) @runner = runner @params = params @@ -11,7 +13,7 @@ class RunnerJobsFinder def execute items = @runner.builds items = by_status(items) - items + sort_items(items) end private @@ -23,4 +25,19 @@ class RunnerJobsFinder items.where(status: params[:status]) end # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + def sort_items(items) + return items unless ALLOWED_INDEXED_COLUMNS.include?(params[:order_by]) + + order_by = params[:order_by] + sort = if /\A(ASC|DESC)\z/i.match?(params[:sort]) + params[:sort] + else + :desc + end + + items.order(order_by => sort) + end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 5615909c4ec..152ebb930e2 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -13,6 +13,7 @@ class GitlabSchema < GraphQL::Schema use BatchLoader::GraphQL use Gitlab::Graphql::Authorize use Gitlab::Graphql::Present + use Gitlab::Graphql::CallsGitaly use Gitlab::Graphql::Connections use Gitlab::Graphql::GenericTracing diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index dd0d9105df6..efeee4a7a4d 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -7,18 +7,34 @@ module Types DEFAULT_COMPLEXITY = 1 def initialize(*args, **kwargs, &block) + @calls_gitaly = !!kwargs.delete(:calls_gitaly) + @constant_complexity = !!kwargs[:complexity] kwargs[:complexity] ||= field_complexity(kwargs[:resolver_class]) super(*args, **kwargs, &block) end + def base_complexity + complexity = DEFAULT_COMPLEXITY + complexity += 1 if calls_gitaly? + complexity + end + + def calls_gitaly? + @calls_gitaly + end + + def constant_complexity? + @constant_complexity + end + private def field_complexity(resolver_class) if resolver_class field_resolver_complexity else - DEFAULT_COMPLEXITY + base_complexity end end @@ -31,6 +47,7 @@ module Types proc do |ctx, args, child_complexity| # Resolvers may add extra complexity depending on used arguments complexity = child_complexity + self.resolver&.try(:resolver_complexity, args, child_complexity: child_complexity).to_i + complexity += 1 if calls_gitaly? field_defn = to_graphql diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 577ccd48ef8..6734d4761c2 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -43,7 +43,7 @@ module Types field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true - field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false + field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false, calls_gitaly: true field :merge_commit_message, GraphQL::STRING_TYPE, method: :default_merge_commit_message, null: true, deprecation_reason: "Renamed to defaultMergeCommitMessage" field :default_merge_commit_message, GraphQL::STRING_TYPE, null: true field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 6ef1d816b7c..bc5fb709522 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -9,6 +9,6 @@ module Types mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle - mount_mutation Mutations::MergeRequests::SetWip + mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true end end diff --git a/app/graphql/types/permission_types/merge_request.rb b/app/graphql/types/permission_types/merge_request.rb index 13995d3ea8f..d877fc177d2 100644 --- a/app/graphql/types/permission_types/merge_request.rb +++ b/app/graphql/types/permission_types/merge_request.rb @@ -10,8 +10,8 @@ module Types abilities :read_merge_request, :admin_merge_request, :update_merge_request, :create_note - permission_field :push_to_source_branch, method: :can_push_to_source_branch? - permission_field :remove_source_branch, method: :can_remove_source_branch? + permission_field :push_to_source_branch, method: :can_push_to_source_branch?, calls_gitaly: true + permission_field :remove_source_branch, method: :can_remove_source_branch?, calls_gitaly: true permission_field :cherry_pick_on_current_merge_request, method: :can_cherry_pick_on_current_merge_request? permission_field :revert_on_current_merge_request, method: :can_revert_on_current_merge_request? end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index c25688ab043..13be71c26ee 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -26,7 +26,7 @@ module Types field :web_url, GraphQL::STRING_TYPE, null: true field :star_count, GraphQL::INT_TYPE, null: false - field :forks_count, GraphQL::INT_TYPE, null: false + field :forks_count, GraphQL::INT_TYPE, null: false, calls_gitaly: true # 4 times field :created_at, Types::TimeType, null: true field :last_activity_at, Types::TimeType, null: true @@ -40,7 +40,7 @@ module Types field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true field :merge_requests_ff_only_enabled, GraphQL::BOOLEAN_TYPE, null: true - field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (project, args, ctx) do + field :avatar_url, GraphQL::STRING_TYPE, null: true, calls_gitaly: true, resolve: -> (project, args, ctx) do project.avatar_url(only_path: false) end diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb index 5987467e1ea..b024eca61fc 100644 --- a/app/graphql/types/repository_type.rb +++ b/app/graphql/types/repository_type.rb @@ -6,9 +6,9 @@ module Types authorize :download_code - field :root_ref, GraphQL::STRING_TYPE, null: true - field :empty, GraphQL::BOOLEAN_TYPE, null: false, method: :empty? + field :root_ref, GraphQL::STRING_TYPE, null: true, calls_gitaly: true + field :empty, GraphQL::BOOLEAN_TYPE, null: false, method: :empty?, calls_gitaly: true field :exists, GraphQL::BOOLEAN_TYPE, null: false, method: :exists? - field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver + field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true end end diff --git a/app/graphql/types/tree/tree_type.rb b/app/graphql/types/tree/tree_type.rb index b947713074e..fbdc1597461 100644 --- a/app/graphql/types/tree/tree_type.rb +++ b/app/graphql/types/tree/tree_type.rb @@ -7,7 +7,7 @@ module Types graphql_name 'Tree' # Complexity 10 as it triggers a Gitaly call on each render - field :last_commit, Types::CommitType, null: true, complexity: 10, resolve: -> (tree, args, ctx) do + field :last_commit, Types::CommitType, null: true, complexity: 10, calls_gitaly: true, resolve: -> (tree, args, ctx) do tree.repository.last_commit_for_path(tree.sha, tree.path) end @@ -15,9 +15,9 @@ module Types Gitlab::Graphql::Representation::TreeEntry.decorate(obj.trees, obj.repository) end - field :submodules, Types::Tree::SubmoduleType.connection_type, null: false + field :submodules, Types::Tree::SubmoduleType.connection_type, null: false, calls_gitaly: true - field :blobs, Types::Tree::BlobType.connection_type, null: false, resolve: -> (obj, args, ctx) do + field :blobs, Types::Tree::BlobType.connection_type, null: false, calls_gitaly: true, resolve: -> (obj, args, ctx) do Gitlab::Graphql::Representation::TreeEntry.decorate(obj.blobs, obj.repository) end # rubocop: enable Graphql/AuthorizeTypes diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index dfadcfc33b2..5476a7cdff6 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -137,7 +137,7 @@ module IssuesHelper end def create_confidential_merge_request_enabled? - Feature.enabled?(:create_confidential_merge_request, @project) + Feature.enabled?(:create_confidential_merge_request, @project, default_enabled: true) end def show_new_branch_button? diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb index 6c3962b4c4f..5b3880f94ba 100644 --- a/app/models/concerns/reactive_caching.rb +++ b/app/models/concerns/reactive_caching.rb @@ -178,7 +178,7 @@ module ReactiveCaching def enqueuing_update(*args) yield - ensure + ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args) end end diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index dbdc8345c93..d2660051343 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -46,7 +46,7 @@ class DroneCiService < CiService end def commit_status(sha, ref) - with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } + with_reactive_cache(sha, ref) { |cached| cached[:commit_status] } end def calculate_reactive_cache(sha, ref) @@ -68,7 +68,7 @@ class DroneCiService < CiService end { commit_status: status } - rescue Errno::ECONNREFUSED + rescue *Gitlab::HTTP::HTTP_ERRORS { commit_status: :error } end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 27b7827d55e..9f5c226f4c9 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -1,18 +1,8 @@ # frozen_string_literal: true -## -# NOTE: -# We'll move this class to Clusters::Platforms::Kubernetes, which contains exactly the same logic. -# After we've migrated data, we'll remove KubernetesService. This would happen in a few months. -# If you're modyfiyng this class, please note that you should update the same change in Clusters::Platforms::Kubernetes. class KubernetesService < Service - include Gitlab::Kubernetes - include ReactiveCaching - default_value_for :category, 'deployment' - self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] } - # Namespace defaults to the project path, but can be overridden in case that # is an invalid or inappropriate name prop_accessor :namespace @@ -47,8 +37,6 @@ class KubernetesService < Service message: Gitlab::Regex.kubernetes_namespace_regex_message } - after_save :clear_reactive_cache! - def self.supported_events %w() end @@ -94,72 +82,6 @@ class KubernetesService < Service ] end - def kubernetes_namespace_for(project) - if namespace.present? - namespace - else - default_namespace - end - end - - # Check we can connect to the Kubernetes API - def test(*args) - kubeclient = build_kube_client! - - kubeclient.core_client.discover - { success: kubeclient.core_client.discovered, result: "Checked API discovery endpoint" } - rescue => err - { success: false, result: err } - end - - # Project param was added on - # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22011, - # as a way to keep this service compatible with - # Clusters::Platforms::Kubernetes, it won't be used on this method - # as it's only needed for Clusters::Cluster. - def predefined_variables(project:) - Gitlab::Ci::Variables::Collection.new.tap do |variables| - variables - .append(key: 'KUBE_URL', value: api_url) - .append(key: 'KUBE_TOKEN', value: token, public: false, masked: true) - .append(key: 'KUBE_NAMESPACE', value: kubernetes_namespace_for(project)) - .append(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true) - - if ca_pem.present? - variables - .append(key: 'KUBE_CA_PEM', value: ca_pem) - .append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true) - end - end - end - - # Constructs a list of terminals from the reactive cache - # - # Returns nil if the cache is empty, in which case you should try again a - # short time later - def terminals(environment) - with_reactive_cache do |data| - project = environment.project - - pods = filter_by_project_environment(data[:pods], project.full_path_slug, environment.slug) - terminals = pods.flat_map { |pod| terminals_for_pod(api_url, kubernetes_namespace_for(project), pod) }.compact - terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) } - end - end - - # Caches resources in the namespace so other calls don't need to block on - # network access - def calculate_reactive_cache - return unless active? && project && !project.pending_delete? - - # We may want to cache extra things in the future - { pods: read_pods } - end - - def kubeclient - @kubeclient ||= build_kube_client! - end - def deprecated? true end @@ -186,14 +108,6 @@ class KubernetesService < Service private - def kubeconfig - to_kubeconfig( - url: api_url, - namespace: kubernetes_namespace_for(project), - token: token, - ca_pem: ca_pem) - end - def namespace_placeholder default_namespace || TEMPLATE_PLACEHOLDER end @@ -205,49 +119,6 @@ class KubernetesService < Service slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') end - def build_kube_client! - raise "Incomplete settings" unless api_url && kubernetes_namespace_for(project) && token - - Gitlab::Kubernetes::KubeClient.new( - api_url, - auth_options: kubeclient_auth_options, - ssl_options: kubeclient_ssl_options, - http_proxy_uri: ENV['http_proxy'] - ) - end - - # Returns a hash of all pods in the namespace - def read_pods - kubeclient = build_kube_client! - - kubeclient.get_pods(namespace: kubernetes_namespace_for(project)).as_json - rescue Kubeclient::ResourceNotFoundError - [] - end - - def kubeclient_ssl_options - opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } - - if ca_pem.present? - opts[:cert_store] = OpenSSL::X509::Store.new - opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) - end - - opts - end - - def kubeclient_auth_options - { bearer_token: token } - end - - def terminal_auth - { - token: token, - ca_pem: ca_pem, - max_session_time: Gitlab::CurrentSettings.terminal_max_session_time - } - end - def enforce_namespace_to_lower_case self.namespace = self.namespace&.downcase end diff --git a/app/models/repository.rb b/app/models/repository.rb index d087a5a7bbd..a408db7ebbe 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -839,10 +839,10 @@ class Repository end end - def merge_to_ref(user, source_sha, merge_request, target_ref, message) + def merge_to_ref(user, source_sha, merge_request, target_ref, message, first_parent_ref) branch = merge_request.target_branch - raw.merge_to_ref(user, source_sha, branch, target_ref, message) + raw.merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref) end def ff_merge(user, source, target_branch, merge_request: nil) diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb index d726085b89a..e06659a39cd 100644 --- a/app/services/auto_merge/base_service.rb +++ b/app/services/auto_merge/base_service.rb @@ -29,7 +29,7 @@ module AutoMerge end def cancel(merge_request) - if cancel_auto_merge(merge_request) + if clear_auto_merge_parameters(merge_request) yield if block_given? success @@ -38,6 +38,16 @@ module AutoMerge end end + def abort(merge_request, reason) + if clear_auto_merge_parameters(merge_request) + yield if block_given? + + success + else + error("Can't abort the automatic merge", 406) + end + end + private def strategy @@ -46,7 +56,7 @@ module AutoMerge end end - def cancel_auto_merge(merge_request) + def clear_auto_merge_parameters(merge_request) merge_request.auto_merge_enabled = false merge_request.merge_user = nil diff --git a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb index cde8c19e8fc..6a33ec071db 100644 --- a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb +++ b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb @@ -19,7 +19,13 @@ module AutoMerge def cancel(merge_request) super do - SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, @project, @current_user) + SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, project, current_user) + end + end + + def abort(merge_request, reason) + super do + SystemNoteService.abort_merge_when_pipeline_succeeds(merge_request, project, current_user, reason) end end diff --git a/app/services/auto_merge_service.rb b/app/services/auto_merge_service.rb index 926d2f5fc66..95bf2db2018 100644 --- a/app/services/auto_merge_service.rb +++ b/app/services/auto_merge_service.rb @@ -42,6 +42,12 @@ class AutoMergeService < BaseService get_service_instance(merge_request.auto_merge_strategy).cancel(merge_request) end + def abort(merge_request, reason) + return error("Can't abort the automatic merge", 406) unless merge_request.auto_merge_enabled? + + get_service_instance(merge_request.auto_merge_strategy).abort(merge_request, reason) + end + def available_strategies(merge_request) self.class.all_strategies.select do |strategy| get_service_instance(strategy).available_for?(merge_request) diff --git a/app/services/discussions/update_diff_position_service.rb b/app/services/discussions/update_diff_position_service.rb index c61437fb2e3..7bdf7711155 100644 --- a/app/services/discussions/update_diff_position_service.rb +++ b/app/services/discussions/update_diff_position_service.rb @@ -3,7 +3,8 @@ module Discussions class UpdateDiffPositionService < BaseService def execute(discussion) - result = tracer.trace(discussion.position) + old_position = discussion.position + result = tracer.trace(old_position) return unless result position = result[:position] diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index c34fbeb2adb..067510a8a0a 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -68,8 +68,8 @@ module MergeRequests !merge_request.for_fork? end - def cancel_auto_merge(merge_request) - AutoMergeService.new(project, current_user).cancel(merge_request) + def abort_auto_merge(merge_request, reason) + AutoMergeService.new(project, current_user).abort(merge_request, reason) end # Returns all origin and fork merge requests from `@project` satisfying passed arguments. diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb index b81a4dd81d2..c2174d2a130 100644 --- a/app/services/merge_requests/close_service.rb +++ b/app/services/merge_requests/close_service.rb @@ -18,7 +18,7 @@ module MergeRequests invalidate_cache_counts(merge_request, users: merge_request.assignees) merge_request.update_project_counter_caches cleanup_environments(merge_request) - cancel_auto_merge(merge_request) + abort_auto_merge(merge_request, 'merge request was closed') end merge_request diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb index efe4dcd6255..0ea50a5dbf5 100644 --- a/app/services/merge_requests/merge_to_ref_service.rb +++ b/app/services/merge_requests/merge_to_ref_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module MergeRequests - # Performs the merge between source SHA and the target branch. Instead + # Performs the merge between source SHA and the target branch or the specified first parent ref. Instead # of writing the result to the MR target branch, it targets the `target_ref`. # # Ideally this should leave the `target_ref` state with the same state the @@ -56,12 +56,22 @@ module MergeRequests raise_error(error) if error end + ## + # The parameter `target_ref` is where the merge result will be written. + # Default is the merge ref i.e. `refs/merge-requests/:iid/merge`. def target_ref - merge_request.merge_ref_path + params[:target_ref] || merge_request.merge_ref_path + end + + ## + # The parameter `first_parent_ref` is the main line of the merge commit. + # Default is the target branch ref of the merge request. + def first_parent_ref + params[:first_parent_ref] || merge_request.target_branch_ref end def commit - repository.merge_to_ref(current_user, source, merge_request, target_ref, commit_message) + repository.merge_to_ref(current_user, source, merge_request, target_ref, commit_message, first_parent_ref) rescue Gitlab::Git::PreReceiveError => error raise MergeError, error.message end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 4b199bd8fa8..8961d2e1023 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -24,7 +24,7 @@ module MergeRequests reload_merge_requests outdate_suggestions refresh_pipelines_on_merge_requests - cancel_auto_merges + abort_auto_merges mark_pending_todos_done cache_merge_requests_closing_issues @@ -142,9 +142,9 @@ module MergeRequests end end - def cancel_auto_merges + def abort_auto_merges merge_requests_for_source_branch.each do |merge_request| - cancel_auto_merge(merge_request) + abort_auto_merge(merge_request, 'source branch was updated') end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 0066cd0491f..d361e96babf 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -44,7 +44,7 @@ module MergeRequests merge_request.previous_changes['target_branch'].first, merge_request.target_branch) - cancel_auto_merge(merge_request) + abort_auto_merge(merge_request, 'target branch was changed') end if merge_request.assignees != old_assignees diff --git a/app/services/prometheus/adapter_service.rb b/app/services/prometheus/adapter_service.rb index 3be958e1613..399f4c35d66 100644 --- a/app/services/prometheus/adapter_service.rb +++ b/app/services/prometheus/adapter_service.rb @@ -27,12 +27,9 @@ module Prometheus end def cluster_prometheus_adapter - return unless deployment_platform.respond_to?(:cluster) + application = deployment_platform&.cluster&.application_prometheus - cluster = deployment_platform.cluster - return unless cluster.application_prometheus&.available? - - cluster.application_prometheus + application if application&.available? end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 4783417ad6d..e4564bc9b00 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -234,6 +234,16 @@ module SystemNoteService create_note(NoteSummary.new(noteable, project, author, body, action: 'merge')) end + # Called when 'merge when pipeline succeeds' is aborted + def abort_merge_when_pipeline_succeeds(noteable, project, author, reason) + body = "aborted the automatic merge because #{reason}" + + ## + # TODO: Abort message should be sent by the system, not a particular user. + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63187. + create_note(NoteSummary.new(noteable, project, author, body, action: 'merge')) + end + def handle_merge_request_wip(noteable, project, author) prefix = noteable.work_in_progress? ? "marked" : "unmarked" @@ -268,11 +278,13 @@ module SystemNoteService merge_request = discussion.noteable diff_refs = change_position.diff_refs version_index = merge_request.merge_request_diffs.viewable.count + position_on_text = change_position.on_text? + text_parts = ["changed this #{position_on_text ? 'line' : 'file'} in"] - text_parts = ["changed this line in"] if version_params = merge_request.version_params_for(diff_refs) - line_code = change_position.line_code(project.repository) - url = url_helpers.diffs_project_merge_request_path(project, merge_request, version_params.merge(anchor: line_code)) + repository = project.repository + anchor = position_on_text ? change_position.line_code(repository) : change_position.file_hash + url = url_helpers.diffs_project_merge_request_path(project, merge_request, version_params.merge(anchor: anchor)) text_parts << "[version #{version_index} of the diff](#{url})" else diff --git a/app/views/layouts/fullscreen.html.haml b/app/views/layouts/fullscreen.html.haml index fa04b5be9f2..91a7777514c 100644 --- a/app/views/layouts/fullscreen.html.haml +++ b/app/views/layouts/fullscreen.html.haml @@ -3,6 +3,7 @@ = render "layouts/head" %body{ class: "#{user_application_theme} #{@body_class} fullscreen-layout", data: { page: body_data_page } } = render 'peek/bar' + = header_message = render partial: "layouts/header/default", locals: { project: @project, group: @group } = render 'shared/outdated_browser' .mobile-overlay @@ -12,3 +13,4 @@ = render "layouts/flash" .content-wrapper{ id: "content-body", class: "d-flex flex-column align-items-stretch mt-0" } = yield + = footer_message diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 4ebfaff0860..d16e2dddbe0 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -92,21 +92,21 @@ .col-lg-8 .form-group %h5= s_('Preferences|Time format') - .checkbox-icon-inline-wrapper.form-check + .checkbox-icon-inline-wrapper - time_format_label = capture do = s_('Preferences|Display time in 24-hour format') - = f.check_box :time_format_in_24h, class: 'form-check-input' + = f.check_box :time_format_in_24h = f.label :time_format_in_24h do = time_format_label %h5= s_('Preferences|Time display') - .checkbox-icon-inline-wrapper.form-check + .checkbox-icon-inline-wrapper - time_display_label = capture do = s_('Preferences|Use relative times') - = f.check_box :time_display_relative, class: 'form-check-input' + = f.check_box :time_display_relative = f.label :time_display_relative do = time_display_label - .text-muted - = s_('Preferences|For example: 30 mins ago.') + .form-text.text-muted + = s_('Preferences|For example: 30 mins ago.') .col-lg-4.profile-settings-sidebar .col-lg-8 .form-group diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 52bb797b5b3..8d3e54dc455 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -3,13 +3,14 @@ - data_action = can_create_merge_request ? 'create-mr' : 'create-branch' - value = can_create_merge_request ? 'Create merge request' : 'Create branch' - value = can_create_confidential_merge_request? ? _('Create confidential merge request') : value + - create_mr_text = can_create_confidential_merge_request? ? _('Create confidential merge request') : _('Create merge request') - can_create_path = can_create_branch_project_issue_path(@project, @issue) - create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch) - create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid) - refs_path = refs_namespace_project_path(@project.namespace, @project, search: '') - .create-mr-dropdown-wrap.d-inline-block.full-width-mobile{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } } + .create-mr-dropdown-wrap.d-inline-block.full-width-mobile.js-create-mr{ data: { project_path: @project.full_path, project_id: @project.id, can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path, is_confidential: can_create_confidential_merge_request?.to_s } } .btn-group.btn-group-sm.unavailable %button.btn.btn-grouped{ type: 'button', disabled: 'disabled' } = icon('spinner', class: 'fa-spin') @@ -26,7 +27,7 @@ .droplab-dropdown %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-right.gl-show-field-errors{ class: ("create-confidential-merge-request-dropdown-menu" if can_create_confidential_merge_request?), data: { dropdown: true } } - if can_create_merge_request - %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: _('Create merge request') } } + %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: create_mr_text } } .menu-item = icon('check', class: 'icon') - if can_create_confidential_merge_request? @@ -41,6 +42,8 @@ %li.divider.droplab-item-ignore %li.droplab-item-ignore.prepend-left-8.append-right-8.prepend-top-16 + - if can_create_confidential_merge_request? + #js-forked-project{ data: { namespace_path: @project.namespace.full_path, project_path: @project.full_path, new_fork_path: new_project_fork_path(@project), help_page_path: help_page_path('user/project/merge_requests') } } .form-group %label{ for: 'new-branch-name' } = _('Branch name') @@ -55,4 +58,8 @@ .form-group %button.btn.btn-success.js-create-target{ type: 'button', data: { action: 'create-mr' } } - = _('Create merge request') + = create_mr_text + + - if can_create_confidential_merge_request? + %p.text-warning.js-exposed-info-warning.hidden + = _('This may expose confidential information as the selected fork is in another namespace that can have other members.') diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb index 25c3a945077..2b36ccb8304 100644 --- a/app/workers/concerns/application_worker.rb +++ b/app/workers/concerns/application_worker.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'sidekiq/api' + Sidekiq::Worker.extend ActiveSupport::Concern module ApplicationWorker @@ -44,6 +46,10 @@ module ApplicationWorker get_sidekiq_options['queue'].to_s end + def queue_size + Sidekiq::Queue.new(queue).size + end + def bulk_perform_async(args_list) Sidekiq::Client.push_bulk('class' => self, 'args' => args_list) end diff --git a/changelogs/unreleased/12550-fullscrean.yml b/changelogs/unreleased/12550-fullscrean.yml new file mode 100644 index 00000000000..f20b191f411 --- /dev/null +++ b/changelogs/unreleased/12550-fullscrean.yml @@ -0,0 +1,5 @@ +--- +title: Removes EE differences for app/views/layouts/fullscreen.html.haml +merge_request: +author: +type: other diff --git a/changelogs/unreleased/12553-preferences.yml b/changelogs/unreleased/12553-preferences.yml new file mode 100644 index 00000000000..c41a8c98e4e --- /dev/null +++ b/changelogs/unreleased/12553-preferences.yml @@ -0,0 +1,5 @@ +--- +title: Removes EE diff for app/views/profiles/preferences/show.html.haml +merge_request: +author: +type: other diff --git a/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml b/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml new file mode 100644 index 00000000000..908a132688c --- /dev/null +++ b/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml @@ -0,0 +1,5 @@ +--- +title: Add order_by and sort params to list runner jobs api +merge_request: 29629 +author: Sujay Patel +type: added diff --git a/changelogs/unreleased/57793-fix-line-age.yml b/changelogs/unreleased/57793-fix-line-age.yml new file mode 100644 index 00000000000..cf4e328e54a --- /dev/null +++ b/changelogs/unreleased/57793-fix-line-age.yml @@ -0,0 +1,5 @@ +--- +title: Support note position tracing on an image +merge_request: 30158 +author: +type: fixed diff --git a/changelogs/unreleased/64176-fix-error-handling.yml b/changelogs/unreleased/64176-fix-error-handling.yml new file mode 100644 index 00000000000..e7a9a5897ae --- /dev/null +++ b/changelogs/unreleased/64176-fix-error-handling.yml @@ -0,0 +1,5 @@ +--- +title: Fix invalid SSL certificate errors on Drone CI service +merge_request: 30422 +author: +type: fixed diff --git a/changelogs/unreleased/Remove-unresolved-class-in-discussion-header.yml b/changelogs/unreleased/Remove-unresolved-class-in-discussion-header.yml new file mode 100644 index 00000000000..3695f3063f3 --- /dev/null +++ b/changelogs/unreleased/Remove-unresolved-class-in-discussion-header.yml @@ -0,0 +1,5 @@ +--- +title: Remove unresolved class and fixed height in discussion header +merge_request: 28440 +author: David Palubin +type: other diff --git a/changelogs/unreleased/create-merge-train-ref-ce.yml b/changelogs/unreleased/create-merge-train-ref-ce.yml new file mode 100644 index 00000000000..b0b95275f58 --- /dev/null +++ b/changelogs/unreleased/create-merge-train-ref-ce.yml @@ -0,0 +1,5 @@ +--- +title: Extend `MergeToRefService` to create merge ref from an arbitrary ref +merge_request: 30361 +author: +type: added diff --git a/changelogs/unreleased/sh-disable-reactive-caching-automatic-retries.yml b/changelogs/unreleased/sh-disable-reactive-caching-automatic-retries.yml new file mode 100644 index 00000000000..a0db68adb78 --- /dev/null +++ b/changelogs/unreleased/sh-disable-reactive-caching-automatic-retries.yml @@ -0,0 +1,5 @@ +--- +title: Prevent amplification of ReactiveCachingWorker jobs upon failures +merge_request: 30432 +author: +type: performance diff --git a/changelogs/unreleased/sh-upgrade-rouge-3-5-1.yml b/changelogs/unreleased/sh-upgrade-rouge-3-5-1.yml new file mode 100644 index 00000000000..b408019c736 --- /dev/null +++ b/changelogs/unreleased/sh-upgrade-rouge-3-5-1.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade Rouge to 3.5.1 +merge_request: 30431 +author: +type: changed diff --git a/changelogs/unreleased/winh-notes-service-applySuggestion.yml b/changelogs/unreleased/winh-notes-service-applySuggestion.yml new file mode 100644 index 00000000000..30e540237b6 --- /dev/null +++ b/changelogs/unreleased/winh-notes-service-applySuggestion.yml @@ -0,0 +1,5 @@ +--- +title: Remove applySuggestion from notes service +merge_request: 30399 +author: Frank van Rest +type: other diff --git a/danger/only_documentation/Dangerfile b/danger/only_documentation/Dangerfile index 8e4564f22b6..0e5f841c891 100644 --- a/danger/only_documentation/Dangerfile +++ b/danger/only_documentation/Dangerfile @@ -1,7 +1,7 @@ # rubocop:disable Style/SignalException # frozen_string_literal: true -has_only_docs_changes = helper.all_changed_files.all? { |file| file.start_with?('doc/') } +has_only_docs_changes = helper.all_changed_files.all? { |file| file.start_with?('doc/', '.gitlab/ci/docs.gitlab-ci.yml') } is_docs_only_branch = gitlab.branch_for_head =~ /(^docs[\/-].*|.*-docs$)/ if is_docs_only_branch && !has_only_docs_changes diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index 7a86fe2eb7c..78ceb74da65 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -146,11 +146,13 @@ class Gitlab::Seeder::CycleAnalytics commit_sha = issue.project.repository.create_file(@user, filename, "content", message: "Commit for #{issue.to_reference}", branch_name: branch_name) issue.project.repository.commit(commit_sha) - GitPushService.new(issue.project, - @user, - oldrev: issue.project.repository.commit("master").sha, - newrev: commit_sha, - ref: 'refs/heads/master').execute + Git::BranchPushService.new( + issue.project, + @user, + oldrev: issue.project.repository.commit("master").sha, + newrev: commit_sha, + ref: 'refs/heads/master' + ).execute branch_name end diff --git a/db/migrate/20190621022810_add_last_ci_minutes_usage_notification_level_to_namespaces.rb b/db/migrate/20190621022810_add_last_ci_minutes_usage_notification_level_to_namespaces.rb new file mode 100644 index 00000000000..1611340284c --- /dev/null +++ b/db/migrate/20190621022810_add_last_ci_minutes_usage_notification_level_to_namespaces.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddLastCiMinutesUsageNotificationLevelToNamespaces < ActiveRecord::Migration[5.1] + DOWNTIME = false + + def change + add_column :namespaces, :last_ci_minutes_usage_notification_level, :integer + end +end diff --git a/db/migrate/20190703130053_remove_gitaly_feature_flags.rb b/db/migrate/20190703130053_remove_gitaly_feature_flags.rb new file mode 100644 index 00000000000..13ac10a5e21 --- /dev/null +++ b/db/migrate/20190703130053_remove_gitaly_feature_flags.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +class RemoveGitalyFeatureFlags < ActiveRecord::Migration[5.1] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + FEATURES = %w[ + gitaly_batch_lfs_pointers gitaly_blame gitaly_blob_get_all_lfs_pointers gitaly_blob_get_new_lfs_pointers + gitaly_branch_names gitaly_branch_names_contains_sha gitaly_branches gitaly_bundle_to_disk + gitaly_calculate_checksum gitaly_can_be_merged gitaly_cherry_pick gitaly_commit_count + gitaly_commit_deltas gitaly_commit_languages gitaly_commit_messages gitaly_commit_patch + gitaly_commit_raw_diffs gitaly_commit_stats gitaly_commit_tree_entry gitaly_commits_between + gitaly_commits_by_message gitaly_conflicts_list_conflict_files gitaly_conflicts_resolve_conflicts gitaly_count_commits + gitaly_count_diverging_commits_no_max gitaly_create_branch gitaly_create_repo_from_bundle gitaly_create_repository + gitaly_delete_branch gitaly_delete_refs gitaly_delta_islands gitaly_deny_disk_acces + gitaly_diff_between gitaly_extract_commit_signature gitaly_fetch_ref gitaly_fetch_remote + gitaly_fetch_source_branch gitaly_filter_shas_with_signature gitaly_filter_shas_with_signatures gitaly_find_all_commits + gitaly_find_branch gitaly_find_commit gitaly_find_commits gitaly_find_ref_name + gitaly_force_push gitaly_fork_repository gitaly_garbage_collect gitaly_get_info_attributes + gitaly_git_blob_load_all_data gitaly_git_blob_raw gitaly_git_fsck gitaly_go-find-all-tags + gitaly_has_local_branches gitaly_import_repository gitaly_is_ancestor gitaly_last_commit_for_path + gitaly_license_short_name gitaly_list_blobs_by_sha_path gitaly_list_commits_by_oid gitaly_local_branches + gitaly_ls_files gitaly_merge_base gitaly_merged_branch_names gitaly_new_commits + gitaly_operation_user_add_tag gitaly_operation_user_commit_file gitaly_operation_user_commit_files gitaly_operation_user_create_branch + gitaly_operation_user_delete_branch gitaly_operation_user_delete_tag gitaly_operation_user_ff_branch gitaly_operation_user_merge_branch + gitaly_post_receive_pack gitaly_post_upload_pack gitaly_project_raw_show gitaly_raw_changes_between + gitaly_rebase gitaly_rebase_in_progress gitaly_ref_delete_refs gitaly_ref_exists + gitaly_ref_exists_branch gitaly_ref_exists_branches gitaly_ref_find_all_remote_branches gitaly_remote_add_remote + gitaly_remote_fetch_internal_remote gitaly_remote_remove_remote gitaly_remote_update_remote_mirror gitaly_remove_namespace + gitaly_repack_full gitaly_repack_incremental gitaly_repository_cleanup gitaly_repository_exists + gitaly_repository_size gitaly_root_ref gitaly_search_files_by_content gitaly_search_files_by_name + gitaly_squash gitaly_squash_in_progress gitaly_ssh_receive_pack gitaly_ssh_upload_pack + gitaly_submodule_url_for gitaly_tag_messages gitaly_tag_names gitaly_tag_names_contains_sha + gitaly_tags gitaly_tree_entries gitaly_wiki_delete_page gitaly_wiki_find_file + gitaly_wiki_find_page gitaly_wiki_get_all_pages gitaly_wiki_page_formatted_data gitaly_wiki_page_versions + gitaly_wiki_update_page gitaly_wiki_write_page gitaly_workhorse_archive gitaly_workhorse_raw_show + gitaly_workhorse_send_git_diff gitaly_workhorse_send_git_patch gitaly_write_config gitaly_write_ref + ] + + class Feature < ActiveRecord::Base + self.table_name = 'features' + end + + def up + Feature.where(key: FEATURES).delete_all + end +end diff --git a/db/schema.rb b/db/schema.rb index 9cc45bb1e47..9a8b64689bd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20190628185004) do +ActiveRecord::Schema.define(version: 20190703130053) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -2116,6 +2116,7 @@ ActiveRecord::Schema.define(version: 20190628185004) do t.integer "extra_shared_runners_minutes_limit" t.string "ldap_sync_status", default: "ready", null: false t.boolean "membership_lock", default: false + t.integer "last_ci_minutes_usage_notification_level" t.index ["created_at"], name: "index_namespaces_on_created_at", using: :btree t.index ["custom_project_templates_group_id", "type"], name: "index_namespaces_on_custom_project_templates_group_id_and_type", where: "(custom_project_templates_group_id IS NOT NULL)", using: :btree t.index ["file_template_project_id"], name: "index_namespaces_on_file_template_project_id", using: :btree diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index 7c7bb9045c7..b98383f30fc 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -441,12 +441,12 @@ and want to eliminate NFS from your environment altogether, there are a few things that you need to do: 1. Make sure the [`git` user home directory](https://docs.gitlab.com/omnibus/settings/configuration.html#moving-the-home-directory-for-a-user) is on local disk. - 1. Configure [database lookup of SSH keys](https://docs.gitlab.com/ce/administration/operations/fast_ssh_key_lookup.html) + 1. Configure [database lookup of SSH keys](../operations/fast_ssh_key_lookup.md) to eliminate the need for a shared authorized_keys file. - 1. Configure [object storage for job artifacts](https://docs.gitlab.com/ce/administration/job_artifacts.html#using-object-storage) - including [live tracing](https://docs.gitlab.com/ce/administration/job_traces.html#new-live-trace-architecture). - 1. Configure [object storage for LFS objects](https://docs.gitlab.com/ce/workflow/lfs/lfs_administration.html#storing-lfs-objects-in-remote-object-storage). - 1. Configure [object storage for uploads](https://docs.gitlab.com/ce/administration/uploads.html#using-object-storage-core-only). + 1. Configure [object storage for job artifacts](../job_artifacts.md#using-object-storage) + including [live tracing](../job_traces.md#new-live-trace-architecture). + 1. Configure [object storage for LFS objects](../../workflow/lfs/lfs_administration.md#storing-lfs-objects-in-remote-object-storage). + 1. Configure [object storage for uploads](../uploads.md#using-object-storage-core-only). NOTE: **Note:** One current feature of GitLab still requires a shared directory (NFS): [GitLab Pages](../../user/project/pages/index.md). There is [work in progress](https://gitlab.com/gitlab-org/gitlab-pages/issues/196) diff --git a/doc/administration/logs.md b/doc/administration/logs.md index b49d8c8a28f..5a2f389d298 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -4,7 +4,7 @@ GitLab has an advanced log system where everything is logged so that you can analyze your instance using various system log files. In addition to system log files, GitLab Enterprise Edition comes with Audit Events. Find more about them [in Audit Events -documentation](https://docs.gitlab.com/ee/administration/audit_events.html) +documentation](audit_events.md) System log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files. diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md index becd480a08f..2b31233d429 100644 --- a/doc/administration/raketasks/maintenance.md +++ b/doc/administration/raketasks/maintenance.md @@ -61,7 +61,7 @@ It will check that each component was set up according to the installation guide You may also have a look at our Troubleshooting Guides: -- [Troubleshooting Guide (GitLab)](http://docs.gitlab.com/ee/README.html#troubleshooting) +- [Troubleshooting Guide (GitLab)](../index.md#troubleshooting) - [Troubleshooting Guide (Omnibus Gitlab)](https://docs.gitlab.com/omnibus/README.html#troubleshooting) **Omnibus Installation** diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md index e780a60a416..98e81217875 100644 --- a/doc/api/group_milestones.md +++ b/doc/api/group_milestones.md @@ -18,13 +18,13 @@ GET /groups/:id/milestones?search=version Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | -| `iids[]` | Array[integer] | optional | Return only the milestones having the given `iid` | -| `state` | string | optional | Return only `active` or `closed` milestones | -| `title` | string | optional | Return only the milestones having the given `title` | -| `search` | string | optional | Return only milestones with a title or description matching the provided string | +| Attribute | Type | Required | Description | +| --------- | ------ | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `iids[]` | integer array | optional | Return only the milestones having the given `iid` | +| `state` | string | optional | Return only `active` or `closed` milestones | +| `title` | string | optional | Return only the milestones having the given `title` | +| `search` | string | optional | Return only milestones with a title or description matching the provided string | ```bash curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/milestones diff --git a/doc/api/issues.md b/doc/api/issues.md index b29626525da..544da1e262c 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -43,12 +43,12 @@ GET /issues?confidential=true | `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | -| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. | +| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. | | `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | -| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | +| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | | `weight` **[STARTER]** | integer | no | Return issues with the specified `weight`. `None` returns issues with no weight assigned. `Any` returns issues with a weight assigned. | -| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | +| `iids[]` | integer array | no | Return only the issues having the given `iid` | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Search issues against their `title` and `description` | @@ -190,13 +190,13 @@ GET /groups/:id/issues?confidential=true | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. | | `with_labels_details` | Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. | -| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | +| `iids[]` | integer array | no | Return only the issues having the given `iid` | | `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | -| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. | +| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. | | `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | -| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | +| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | | `weight` **[STARTER]** | integer | no | Return issues with the specified `weight`. `None` returns issues with no weight assigned. `Any` returns issues with a weight assigned. | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | @@ -336,16 +336,16 @@ GET /projects/:id/issues?confidential=true | Attribute | Type | Required | Description | | ------------------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` | +| `iids[]` | integer array | no | Return only the milestone having the given `iid` | | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. | | `with_labels_details` | Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. | | `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | -| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. | +| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. | | `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | -| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | +| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | | `weight` **[STARTER]** | integer | no | Return issues with the specified `weight`. `None` returns issues with no weight assigned. `Any` returns issues with a weight assigned. | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | @@ -596,7 +596,7 @@ POST /projects/:id/issues | `title` | string | yes | The title of an issue | | `description` | string | no | The description of an issue | | `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. | -| `assignee_ids` | Array[integer] | no | The ID of a user to assign issue | +| `assignee_ids` | integer array | no | The ID of a user to assign issue | | `milestone_id` | integer | no | The global ID of a milestone to assign issue | | `labels` | string | no | Comma-separated label names for an issue | | `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project/group owner rights) | @@ -697,7 +697,7 @@ PUT /projects/:id/issues/:issue_iid | `title` | string | no | The title of an issue | | `description` | string | no | The description of an issue | | `confidential` | boolean | no | Updates an issue to be confidential | -| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. | +| `assignee_ids` | integer array | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. | | `milestone_id` | integer | no | The global ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.| | `labels` | string | no | Comma-separated label names for an issue. Set to an empty string to unassign all labels. | | `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it | diff --git a/doc/api/issues_statistics.md b/doc/api/issues_statistics.md index 82bc9c142cc..58a32e879d7 100644 --- a/doc/api/issues_statistics.md +++ b/doc/api/issues_statistics.md @@ -34,9 +34,9 @@ GET /issues_statistics?confidential=true | `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. | | `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. | | `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. | -| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | +| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. | -| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | +| `iids[]` | integer array | no | Return only the issues having the given `iid` | | `search` | string | no | Search issues against their `title` and `description` | | `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` | | `created_after` | datetime | no | Return issues created on or after the given time | @@ -86,13 +86,13 @@ GET /groups/:id/issues_statistics?confidential=true | ------------------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. | -| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | +| `iids[]` | integer array | no | Return only the issues having the given `iid` | | `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. | | `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. | | `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. | | `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. | -| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | +| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. | | `search` | string | no | Search group issues against their `title` and `description` | | `created_after` | datetime | no | Return issues created on or after the given time | @@ -141,14 +141,14 @@ GET /projects/:id/issues_statistics?confidential=true | Attribute | Type | Required | Description | | ------------------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` | +| `iids[]` | integer array | no | Return only the milestone having the given `iid` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. | | `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. | | `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. | | `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. | | `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. | -| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | +| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. | | `search` | string | no | Search project issues against their `title` and `description` | | `created_after` | datetime | no | Return issues created on or after the given time | diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index eeb035ef49c..850e3be42d5 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -192,7 +192,7 @@ Parameters: | Attribute | Type | Required | Description | | ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | | `id` | integer | yes | The ID of a project | -| `iids[]` | Array[integer] | no | Return the request having the given `iid` | +| `iids[]` | integer array | no | Return the request having the given `iid` | | `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` | | `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | @@ -831,12 +831,12 @@ POST /projects/:id/merge_requests | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `source_branch` | string | yes | The source branch | | `target_branch` | string | yes | The target branch | | `title` | string | yes | Title of MR | | `assignee_id` | integer | no | Assignee user ID | -| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. | +| `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. | | `description` | string | no | Description of MR | | `target_project_id` | integer | no | The target project (numeric id) | | `labels` | string | no | Labels for MR as a comma-separated list | @@ -987,7 +987,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid | `target_branch` | string | no | The target branch | | `title` | string | no | Title of MR | | `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. | -| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. | +| `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. | | `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.| | `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. | | `description` | string | no | Description of MR | diff --git a/doc/api/milestones.md b/doc/api/milestones.md index e745d0c2e6c..8f69378dac3 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -16,13 +16,13 @@ GET /projects/:id/milestones?search=version Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `iids[]` | Array[integer] | optional | Return only the milestones having the given `iid` | -| `state` | string | optional | Return only `active` or `closed` milestones | -| `title` | string | optional | Return only the milestones having the given `title` | -| `search` | string | optional | Return only milestones with a title or description matching the provided string | +| Attribute | Type | Required | Description | +| --------- | ------ | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `iids[]` | integer array | optional | Return only the milestones having the given `iid` | +| `state` | string | optional | Return only `active` or `closed` milestones | +| `title` | string | optional | Return only the milestones having the given `title` | +| `search` | string | optional | Return only milestones with a title or description matching the provided string | ```bash curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/milestones diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md index 753faec3cc8..e36f74e7c77 100644 --- a/doc/api/pipelines.md +++ b/doc/api/pipelines.md @@ -132,11 +132,11 @@ Example of response POST /projects/:id/pipeline ``` -| Attribute | Type | Required | Description | -|------------|---------|----------|---------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `ref` | string | yes | Reference to commit | -| `variables` | array | no | An array containing the variables available in the pipeline, matching the structure [{ 'key' => 'UPLOAD_TO_S3', 'variable_type' => 'file', 'value' => 'true' }] | +| Attribute | Type | Required | Description | +|-------------|---------|----------|---------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `ref` | string | yes | Reference to commit | +| `variables` | array | no | An array containing the variables available in the pipeline, matching the structure `[{ 'key' => 'UPLOAD_TO_S3', 'variable_type' => 'file', 'value' => 'true' }]` | ``` curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipeline?ref=master" diff --git a/doc/api/runners.md b/doc/api/runners.md index 2d91428d1c1..1318b9ca828 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -44,7 +44,7 @@ GET /runners?tag_list=tag1,tag2 | `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of specific runners to show, one of: `active`, `paused`, `online`, `offline`; showing all runners if none provided | | `type` | string | no | The type of runners to show, one of: `instance_type`, `group_type`, `project_type` | | `status` | string | no | The status of runners to show, one of: `active`, `paused`, `online`, `offline` | -| `tag_list` | Array[String] | no | List of of the runner's tags | +| `tag_list` | string array | no | List of of the runner's tags | ``` curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners" @@ -95,7 +95,7 @@ GET /runners/all?tag_list=tag1,tag2 | `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of runners to show, one of: `specific`, `shared`, `active`, `paused`, `online`, `offline`; showing all runners if none provided | | `type` | string | no | The type of runners to show, one of: `instance_type`, `group_type`, `project_type` | | `status` | string | no | The status of runners to show, one of: `active`, `paused`, `online`, `offline` | -| `tag_list` | Array[String] | no | List of of the runner's tags | +| `tag_list` | string array | no | List of of the runner's tags | ``` curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/all" @@ -214,10 +214,10 @@ PUT /runners/:id | `description` | string | no | The description of a runner | | `active` | boolean | no | The state of a runner; can be set to `true` or `false` | | `tag_list` | array | no | The list of tags for a runner; put array of tags, that should be finally assigned to a runner | -| `run_untagged` | boolean | no | Flag indicating the runner can execute untagged jobs | -| `locked` | boolean | no | Flag indicating the runner is locked | -| `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` | -| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job | +| `run_untagged`| boolean | no | Flag indicating the runner can execute untagged jobs | +| `locked` | boolean | no | Flag indicating the runner is locked | +| `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` | +| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job | ``` curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2" @@ -291,6 +291,8 @@ GET /runners/:id/jobs |-----------|---------|----------|---------------------| | `id` | integer | yes | The ID of a runner | | `status` | string | no | Status of the job; one of: `running`, `success`, `failed`, `canceled` | +| `order_by`| string | no | Order jobs by `id`. | +| `sort` | string | no | Sort jobs in `asc` or `desc` order (default: `desc`) | ``` curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/1/jobs?status=running" @@ -385,7 +387,7 @@ GET /projects/:id/runners?tag_list=tag1,tag2 | `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of specific runners to show, one of: `active`, `paused`, `online`, `offline`; showing all runners if none provided | | `type` | string | no | The type of runners to show, one of: `instance_type`, `group_type`, `project_type` | | `status` | string | no | The status of runners to show, one of: `active`, `paused`, `online`, `offline` | -| `tag_list` | Array[String] | no | List of of the runner's tags | +| `tag_list` | string array | no | List of of the runner's tags | ``` curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/9/runners" @@ -477,17 +479,17 @@ Register a new Runner for the instance. POST /runners ``` -| Attribute | Type | Required | Description | -|-------------|---------|----------|---------------------| -| `token` | string | yes | [Registration token](#registration-and-authentication-tokens). | -| `description`| string | no | Runner's description| -| `info` | hash | no | Runner's metadata | -| `active` | boolean| no | Whether the Runner is active | -| `locked` | boolean| no | Whether the Runner should be locked for current project | -| `run_untagged` | boolean | no | Whether the Runner should handle untagged jobs | -| `tag_list` | Array[String] | no | List of Runner's tags | -| `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` | -| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job | +| Attribute | Type | Required | Description | +|--------------|---------|----------|---------------------| +| `token` | string | yes | [Registration token](#registration-and-authentication-tokens). | +| `description`| string | no | Runner's description| +| `info` | hash | no | Runner's metadata | +| `active` | boolean | no | Whether the Runner is active | +| `locked` | boolean | no | Whether the Runner should be locked for current project | +| `run_untagged` | boolean | no | Whether the Runner should handle untagged jobs | +| `tag_list` | string array | no | List of Runner's tags | +| `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` | +| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job | ``` curl --request POST "https://gitlab.example.com/api/v4/runners" --form "token=<registration_token>" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2" diff --git a/doc/api/services.md b/doc/api/services.md index c811d0e84ca..4f35c17e927 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -583,7 +583,7 @@ Parameters: | `username` | string | yes | The username of the user created to be used with GitLab/Jira. | | `password` | string | yes | The password of the user created to be used with GitLab/Jira. | | `active` | boolean | no | Activates or deactivates the service. Defaults to false (deactivated). | -| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the Jira workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | +| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the Jira workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column. By default, this ID is set to `2`. | | `commit_events` | boolean | false | Enable notifications for commit events | | `merge_requests_events` | boolean | false | Enable notifications for merge request events | diff --git a/doc/api/vulnerabilities.md b/doc/api/vulnerabilities.md index 390d0966244..f1b11c7bceb 100644 --- a/doc/api/vulnerabilities.md +++ b/doc/api/vulnerabilities.md @@ -32,13 +32,13 @@ GET /projects/:id/vulnerabilities?severity=high GET /projects/:id/vulnerabilities?confidence=unknown,experimental ``` -| Attribute | Type | Required | Description | -| ------------------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| `report_type` | Array[string] | no | Returns vulnerabilities belonging to specified report type. Valid values: `sast`, `dast`, `dependency_scanning`, or `container_scanning`. | -| `scope` | string | no | Returns vulnerabilities for the given scope: `all` or `dismissed`. Defaults to `dismissed` | -| `severity` | Array[string] | no | Returns vulnerabilities belonging to specified severity level: `undefined`, `info`, `unknown`, `low`, `medium`, `high`, or `critical`. Defaults to all' | -| `confidence` | Array[string] | no | Returns vulnerabilities belonging to specified confidence level: `undefined`, `ignore`, `unknown`, `experimental`, `low`, `medium`, `high`, or `confirmed`. Defaults to all | +| Attribute | Type | Required | Description | +| ------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `report_type` | string array | no | Returns vulnerabilities belonging to specified report type. Valid values: `sast`, `dast`, `dependency_scanning`, or `container_scanning`. | +| `scope` | string | no | Returns vulnerabilities for the given scope: `all` or `dismissed`. Defaults to `dismissed` | +| `severity` | string array | no | Returns vulnerabilities belonging to specified severity level: `undefined`, `info`, `unknown`, `low`, `medium`, `high`, or `critical`. Defaults to all' | +| `confidence` | string array | no | Returns vulnerabilities belonging to specified confidence level: `undefined`, `ignore`, `unknown`, `experimental`, `low`, `medium`, `high`, or `confirmed`. Defaults to all | ```bash curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/vulnerabilities diff --git a/doc/ci/ci_cd_for_external_repos/github_integration.md b/doc/ci/ci_cd_for_external_repos/github_integration.md index 53b36181062..36031c7929f 100644 --- a/doc/ci/ci_cd_for_external_repos/github_integration.md +++ b/doc/ci/ci_cd_for_external_repos/github_integration.md @@ -109,7 +109,7 @@ your repository: new commits. The web hook URL should be set to the GitLab API to - [trigger pull mirroring](https://docs.gitlab.com/ee/api/projects.html#start-the-pull-mirroring-process-for-a-project-starter), + [trigger pull mirroring](../../api/projects.md#start-the-pull-mirroring-process-for-a-project-starter), using the GitLab personal access token we just created. ``` diff --git a/doc/development/README.md b/doc/development/README.md index 5df6ec5fd56..1566173992a 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -82,6 +82,7 @@ description: 'Learn how to contribute to GitLab.' - [Understanding EXPLAIN plans](understanding_explain_plans.md) - [explain.depesz.com](https://explain.depesz.com/) for visualising the output of `EXPLAIN` +- [pgFormatter](http://sqlformat.darold.net/) a PostgreSQL SQL syntax beautifier ### Migrations diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md index aeddad14995..c83a0427c98 100644 --- a/doc/development/api_graphql_styleguide.md +++ b/doc/development/api_graphql_styleguide.md @@ -2,13 +2,14 @@ ## Deep Dive -In March 2019, Nick Thomas hosted a [Deep Dive] on GitLab's [GraphQL API] to share his domain specific knowledge with anyone who may work in this part of the code base in the future. You can find the [recording on YouTube], and the slides on [Google Slides] and in [PDF]. Everything covered in this deep dive was accurate as of GitLab 11.9, and while specific details may have changed since then, it should still serve as a good introduction. - -[Deep Dive]: https://gitlab.com/gitlab-org/create-stage/issues/1 -[Pull Repository Mirroring functionality]: ../api/graphql/ -[recording on YouTube]: https://www.youtube.com/watch?v=-9L_1MWrjkg -[Google Slides]: https://docs.google.com/presentation/d/1qOTxpkTdHIp1CRjuTvO-aXg0_rUtzE3ETfLUdnBB5uQ/edit -[PDF]: https://gitlab.com/gitlab-org/create-stage/uploads/8e78ea7f326b2ef649e7d7d569c26d56/GraphQL_Deep_Dive__Create_.pdf +In March 2019, Nick Thomas hosted a [Deep Dive](https://gitlab.com/gitlab-org/create-stage/issues/1) +on GitLab's [GraphQL API](../api/graphql/index.md) to share his domain specific knowledge +with anyone who may work in this part of the code base in the future. You can find the +[recording on YouTube](https://www.youtube.com/watch?v=-9L_1MWrjkg), and the slides on +[Google Slides](https://docs.google.com/presentation/d/1qOTxpkTdHIp1CRjuTvO-aXg0_rUtzE3ETfLUdnBB5uQ/edit) +and in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/8e78ea7f326b2ef649e7d7d569c26d56/GraphQL_Deep_Dive__Create_.pdf). +Everything covered in this deep dive was accurate as of GitLab 11.9, and while specific +details may have changed since then, it should still serve as a good introduction. ## Authentication diff --git a/doc/development/architecture.md b/doc/development/architecture.md index ee6d00331e3..8319603fea2 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -267,7 +267,7 @@ GitLab CI is the open-source continuous integration service included with GitLab #### Gitlab Workhorse - [Project page](https://gitlab.com/gitlab-org/gitlab-workhorse/blob/master/README.md) -- Configuration: [Omnibus][gitlab-workhorse-omnibus], [Charts][gitlab-workhorse-charts], [Source][workhorse-source] +- Configuration: [Omnibus][workhorse-omnibus], [Charts][workhorse-charts], [Source][workhorse-source] - Layer: Core Service (Processor) - Process: `gitlab-workhorse` @@ -681,7 +681,7 @@ We've also detailed [our architecture of GitLab.com](https://about.gitlab.com/ha [pgbouncer-exporter-omnibus]: ../administration/monitoring/prometheus/pgbouncer_exporter.md [pgbouncer-exporter-charts]: https://docs.gitlab.com/charts/installation/deployment.html#postgresql [gitlab-monitor-omnibus]: ../administration/monitoring/prometheus/gitlab_monitor_exporter.md -[gitab-monitor-charts]: https://docs.gitlab.com/charts/charts/gitlab/gitlab-monitor/index.html +[gitlab-monitor-charts]: https://docs.gitlab.com/charts/charts/gitlab/gitlab-monitor/index.html [node-exporter-omnibus]: ../administration/monitoring/prometheus/node_exporter.md [node-exporter-charts]: https://gitlab.com/charts/gitlab/issues/1332 [mattermost-omnibus]: https://docs.gitlab.com/omnibus/gitlab-mattermost/ diff --git a/doc/development/automatic_ce_ee_merge.md b/doc/development/automatic_ce_ee_merge.md index 3ca841353e6..423b35a9e3a 100644 --- a/doc/development/automatic_ce_ee_merge.md +++ b/doc/development/automatic_ce_ee_merge.md @@ -115,7 +115,7 @@ Now, every time you create an MR for CE and EE: 1. Continue cherry-picking: `git cherry-pick --continue` 1. Push to EE: `git push origin branch-example-ee` 1. Create the EE-equivalent MR and link to the CE MR from the - description "Ports [CE-MR-LINK] to EE" + description `Ports [CE-MR-LINK] to EE` 1. Once all the jobs are passing in both CE and EE, you've addressed the feedback from your own team, and got them approved, the merge requests can be merged. 1. When both MRs are ready, the EE merge request will be merged first, and the @@ -125,42 +125,43 @@ Now, every time you create an MR for CE and EE: - The commit SHA can be easily found from the GitLab UI. From a merge request, open the tab **Commits** and click the copy icon to copy the commit SHA. -- To cherry-pick a **commit range**, such as [A > B > C > D] use: +- To cherry-pick a **commit range**, such as (A > B > C > D) use: - ```shell - git cherry-pick "oldest-commit-SHA^..newest-commit-SHA" - ``` + ```shell + git cherry-pick "oldest-commit-SHA^..newest-commit-SHA" + ``` - For example, suppose the commit A is the oldest, and its SHA is `4f5e4018c09ed797fdf446b3752f82e46f5af502`, - and the commit D is the newest, and its SHA is `80e1c9e56783bd57bd7129828ec20b252ebc0538`. - The cherry-pick command will be: + For example, suppose the commit A is the oldest, and its SHA is `4f5e4018c09ed797fdf446b3752f82e46f5af502`, + and the commit D is the newest, and its SHA is `80e1c9e56783bd57bd7129828ec20b252ebc0538`. + The cherry-pick command will be: - ```shell - git cherry-pick "4f5e4018c09ed797fdf446b3752f82e46f5af502^..80e1c9e56783bd57bd7129828ec20b252ebc0538" - ``` + ```shell + git cherry-pick "4f5e4018c09ed797fdf446b3752f82e46f5af502^..80e1c9e56783bd57bd7129828ec20b252ebc0538" + ``` - To cherry-pick a **merge commit**, use the flag `-m 1`. For example, suppose that the merge commit SHA is `138f5e2f20289bb376caffa0303adb0cac859ce1`: - ```shell - git cherry-pick -m 1 138f5e2f20289bb376caffa0303adb0cac859ce1 - ``` -- To cherry-pick multiple commits, such as B and D in a range [A > B > C > D], use: + ```shell + git cherry-pick -m 1 138f5e2f20289bb376caffa0303adb0cac859ce1 + ``` - ```shell - git cherry-pick commit-B-SHA commit-D-SHA - ``` +- To cherry-pick multiple commits, such as B and D in a range (A > B > C > D), use: - For example, suppose commit B SHA = `4f5e4018c09ed797fdf446b3752f82e46f5af502`, - and the commit D SHA = `80e1c9e56783bd57bd7129828ec20b252ebc0538`. - The cherry-pick command will be: + ```shell + git cherry-pick commit-B-SHA commit-D-SHA + ``` - ```shell - git cherry-pick 4f5e4018c09ed797fdf446b3752f82e46f5af502 80e1c9e56783bd57bd7129828ec20b252ebc0538 - ``` + For example, suppose commit B SHA = `4f5e4018c09ed797fdf446b3752f82e46f5af502`, + and the commit D SHA = `80e1c9e56783bd57bd7129828ec20b252ebc0538`. + The cherry-pick command will be: - This case is particularly useful when you have a merge commit in a sequence of - commits and you want to cherry-pick all but the merge commit. + ```shell + git cherry-pick 4f5e4018c09ed797fdf446b3752f82e46f5af502 80e1c9e56783bd57bd7129828ec20b252ebc0538 + ``` + + This case is particularly useful when you have a merge commit in a sequence of + commits and you want to cherry-pick all but the merge commit. - If you push more commits to the CE branch, you can safely repeat the procedure to cherry-pick them to the EE-equivalent branch. You can do that as many times as diff --git a/doc/development/changelog.md b/doc/development/changelog.md index 45b3d5a23a1..f13c447fef2 100644 --- a/doc/development/changelog.md +++ b/doc/development/changelog.md @@ -35,7 +35,7 @@ the `author` field. GitLab team members **should not**. - Any user-facing change **should** have a changelog entry. Example: "GitLab now uses system fonts for all text." -- Any change behind a feature flag **should not** have a changelog entry. The entry should be added [in the merge request removing the feature flags](https://docs.gitlab.com/ee/development/feature_flags.html#developing-with-feature-flags). +- Any change behind a feature flag **should not** have a changelog entry. The entry should be added [in the merge request removing the feature flags](feature_flags/development.md). - A fix for a regression introduced and then fixed in the same release (i.e., fixing a bug introduced during a monthly release candidate) **should not** have a changelog entry. @@ -62,7 +62,7 @@ making it both concise and descriptive, err on the side of descriptive. The first example provides no context of where the change was made, or why, or how it benefits the user. -- **Bad:** Copy [some text] to clipboard. +- **Bad:** Copy (some text) to clipboard. - **Good:** Update the "Copy to clipboard" tooltip to indicate what's being copied. diff --git a/doc/development/chatops_on_gitlabcom.md b/doc/development/chatops_on_gitlabcom.md index a7b402c3fb0..de942c0ae94 100644 --- a/doc/development/chatops_on_gitlabcom.md +++ b/doc/development/chatops_on_gitlabcom.md @@ -17,6 +17,6 @@ To request access to Chatops on GitLab.com: ## See also - - [Chatops Usage](https://docs.gitlab.com/ee/ci/chatops/README.html) + - [Chatops Usage](../ci/chatops/README.md) - [Understanding EXPLAIN plans](understanding_explain_plans.md) - [Feature Groups](feature_flags/development.md#feature-groups) diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md index f4fa0caeb01..27c349c03aa 100644 --- a/doc/development/contributing/issue_workflow.md +++ b/doc/development/contributing/issue_workflow.md @@ -345,7 +345,7 @@ For feature proposals for EE, open an issue on the In order to help track the feature proposals, we have created a [`feature`][fl] label. For the time being, users that are not members -of the project cannot add labels. You can instead ask one of the [core team] +of the project cannot add labels. You can instead ask one of the [core team](https://about.gitlab.com/community/core-team/) members to add the label ~feature to the issue or add the following code snippet right after your description in a new line: `~feature`. @@ -356,7 +356,7 @@ Please submit Feature Proposals using the ['Feature Proposal' issue template](ht For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may -need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself. +need to ask one of the [core team](https://about.gitlab.com/community/core-team/) members to add the label, if you do not have permissions to do it by yourself. If you want to create something yourself, consider opening an issue first to discuss whether it is interesting to include this in GitLab. diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md index de2c5b43411..0311eda1ff1 100644 --- a/doc/development/database_debugging.md +++ b/doc/development/database_debugging.md @@ -3,7 +3,7 @@ This section is to help give some copy-pasta you can use as a reference when you run into some head-banging database problems. -An easy first step is to search for your error in Slack or google "GitLab <my error>". +An easy first step is to search for your error in Slack or google "GitLab (my error)". --- diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index 6bfedcb1047..d0a887b6175 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -393,7 +393,7 @@ Instead: Example: ```md -For more information, see the [confidential issue](https://docs.gitlab.com/ee/user/project/issues/confidential_issues.html) `https://gitlab.com/gitlab-org/gitlab-ce/issues/<issue_number>`. +For more information, see the [confidential issue](../../user/project/issues/confidential_issues.md) `https://gitlab.com/gitlab-org/gitlab-ce/issues/<issue_number>`. ``` ### Unlinking emails diff --git a/doc/development/elasticsearch.md b/doc/development/elasticsearch.md index 603a756ff56..e5da47ba16e 100644 --- a/doc/development/elasticsearch.md +++ b/doc/development/elasticsearch.md @@ -150,7 +150,7 @@ Uses an [Edge NGram token filter](https://www.elastic.co/guide/en/elasticsearch/ ## Troubleshooting -### Getting "flood stage disk watermark [95%] exceeded" +### Getting `flood stage disk watermark [95%] exceeded` You might get an error such as diff --git a/doc/development/fe_guide/development_process.md b/doc/development/fe_guide/development_process.md index f3fdaa3b883..ae0e2361840 100644 --- a/doc/development/fe_guide/development_process.md +++ b/doc/development/fe_guide/development_process.md @@ -12,8 +12,7 @@ This checklist is intended to help us during development of bigger features/refa Please use your best judgement when to use it and please contribute new points through merge requests if something comes to your mind. ---- - +``` ### Frontend development #### Planning development @@ -24,15 +23,15 @@ Please use your best judgement when to use it and please contribute new points t - [ ] Are all necessary UX specifications available that you will need in order to implement? Are there new UX components/patterns in the designs? Then contact the UI component team early on. How should error messages or validation be handled? - [ ] **Library usage** Use Vuex as soon as you have even a medium state to manage, use Vue router if you need to have different views internally and want to link from the outside. Check what libraries we already have for which occasions. - [ ] **Plan your implementation:** - - [ ] **Architecture plan:** Create a plan aligned with GitLab's architecture, how you are going to do the implementation, for example Vue application setup and its components (through [onion skinning](https://gitlab.com/gitlab-org/gitlab-ce/issues/35873#note_39994091)), Store structure and data flow, which existing Vue components can you reuse. It's a good idea to go through your plan with another engineer to refine it. - - [ ] **Backend:** The best way is to kickoff the implementation in a call and discuss with the assigned Backend engineer what you will need from the backend and also when. Can you reuse existing API's? How is the performance with the planned architecture? Maybe create together a JSON mock object to already start with development. - - [ ] **Communication:** It also makes sense to have for bigger features an own slack channel (normally called #f_{feature_name}) and even weekly demo calls with all people involved. - - [ ] **Dependency Plan:** Are there big dependencies in the plan between you and others, then maybe create an execution diagram to show what is blocking which part and the order of the different parts. - - [ ] **Task list:** Create a simple checklist of the subtasks that are needed for the implementation, also consider creating even sub issues. (for example show a comment, delete a comment, update a comment, etc.). This helps you and also everyone else following the implementation + - [ ] **Architecture plan:** Create a plan aligned with GitLab's architecture, how you are going to do the implementation, for example Vue application setup and its components (through [onion skinning](https://gitlab.com/gitlab-org/gitlab-ce/issues/35873#note_39994091)), Store structure and data flow, which existing Vue components can you reuse. It's a good idea to go through your plan with another engineer to refine it. + - [ ] **Backend:** The best way is to kickoff the implementation in a call and discuss with the assigned Backend engineer what you will need from the backend and also when. Can you reuse existing API's? How is the performance with the planned architecture? Maybe create together a JSON mock object to already start with development. + - [ ] **Communication:** It also makes sense to have for bigger features an own slack channel (normally called #f_{feature_name}) and even weekly demo calls with all people involved. + - [ ] **Dependency Plan:** Are there big dependencies in the plan between you and others, then maybe create an execution diagram to show what is blocking which part and the order of the different parts. + - [ ] **Task list:** Create a simple checklist of the subtasks that are needed for the implementation, also consider creating even sub issues. (for example show a comment, delete a comment, update a comment, etc.). This helps you and also everyone else following the implementation - [ ] **Keep it small** To make it easier for you and also all reviewers try to keep merge requests small and merge into a feature branch if needed. To accomplish that you need to plan that from the start. Different methods are: - - [ ] **Skeleton based plan** Start with an MR that has the skeleton of the components with placeholder content. In following MRs you can fill the components with interactivity. This also makes it easier to spread out development on multiple people. - - [ ] **Cookie Mode** Think about hiding the feature behind a cookie flag if the implementation is on top of existing features - - [ ] **New route** Are you refactoring something big then you might consider adding a new route where you implement the new feature and when finished delete the current route and rename the new one. (for example 'merge_request' and 'new_merge_request') + - [ ] **Skeleton based plan** Start with an MR that has the skeleton of the components with placeholder content. In following MRs you can fill the components with interactivity. This also makes it easier to spread out development on multiple people. + - [ ] **Cookie Mode** Think about hiding the feature behind a cookie flag if the implementation is on top of existing features + - [ ] **New route** Are you refactoring something big then you might consider adding a new route where you implement the new feature and when finished delete the current route and rename the new one. (for example 'merge_request' and 'new_merge_request') - [ ] **Setup** Is there any specific setup needed for your implementation (for example a kubernetes cluster)? Then let everyone know if it is not already mentioned where they can find documentation (if it doesn't exist - create it) - [ ] **Security** Are there any new security relevant implementations? Then please contact the security team for an app security review. If you are not sure ask our [domain expert](https://about.gitlab.com/handbook/engineering/frontend/#frontend-domain-experts) @@ -57,8 +56,7 @@ Please use your best judgement when to use it and please contribute new points t - [ ] Are there any big changes on how and especially how frequently we use the API then let production know about it - [ ] Smoke test of the RC on dev., staging., canary deployments and .com - [ ] Follow up on issues that came out of the review. Create issues for discovered edge cases that should be covered in future iterations. - ---- +``` ### Share your work early @@ -66,7 +64,7 @@ Please use your best judgement when to use it and please contribute new points t GitLab's architecture. 1. Add a diagram to the issue and ask a frontend architect in the slack channel `#fe_architectural` about it. - ![Diagram of Issue Boards Architecture](img/boards_diagram.png) + ![Diagram of Issue Boards Architecture](img/boards_diagram.png) 1. Don't take more than one week between starting work on a feature and sharing a Merge Request with a reviewer or a maintainer. diff --git a/doc/development/feature_flags/controls.md b/doc/development/feature_flags/controls.md index c67467b7c11..739f4207e27 100644 --- a/doc/development/feature_flags/controls.md +++ b/doc/development/feature_flags/controls.md @@ -5,7 +5,7 @@ GitLab Inc. provided environments such as staging and production, you need to have access to the chatops bot. Chatops bot is currently running on the ops instance, which is different from GitLab.com or dev.gitlab.org. -Follow the Chatops document to [request access](https://docs.gitlab.com/ee/development/chatops_on_gitlabcom.html#requesting-access). +Follow the Chatops document to [request access](../chatops_on_gitlabcom.md#requesting-access). Once you are added to the project test if your access propagated, run: @@ -112,7 +112,7 @@ instances. Make sure to add the ~"feature flag" label to this merge request so release managers are aware the changes are hidden behind a feature flag. If the merge request has to be picked into a stable branch, make sure to also add the appropriate "Pick into X" label (e.g. "Pick into XX.X"). -See [the process document](https://docs.gitlab.com/ee/development/feature_flags/process.html#including-a-feature-behind-feature-flag-in-the-final-release) for further details. +See [the process document](process.md#including-a-feature-behind-feature-flag-in-the-final-release) for further details. When a feature gate has been removed from the code base, the value still exists in the database. diff --git a/doc/development/feature_flags/development.md b/doc/development/feature_flags/development.md index 238052529d9..98773026122 100644 --- a/doc/development/feature_flags/development.md +++ b/doc/development/feature_flags/development.md @@ -57,7 +57,7 @@ the feature flag check will default to `true`. As an example, if you were to ship the backend half of a feature behind a flag, you'd want to explicitly disable that flag until the frontend half is also ready -to be shipped. [You can do this via Chatops](https://docs.gitlab.com/ee/development/feature_flags/controls.html): +to be shipped. [You can do this via Chatops](controls.md): ``` /chatops run feature set some_feature 0 diff --git a/doc/development/file_storage.md b/doc/development/file_storage.md index 18e4dc2ca0c..02874d18a30 100644 --- a/doc/development/file_storage.md +++ b/doc/development/file_storage.md @@ -33,9 +33,9 @@ they are still not 100% standardized. You can see them below: | User avatars | yes | uploads/-/system/user/avatar/:id/:filename | `AvatarUploader` | User | | User snippet attachments | yes | uploads/-/system/personal_snippet/:id/:random_hex/:filename | `PersonalFileUploader` | Snippet | | Project avatars | yes | uploads/-/system/project/avatar/:id/:filename | `AvatarUploader` | Project | -| Issues/MR/Notes Markdown attachments | yes | uploads/:project_path_with_namespace/:random_hex/:filename | `FileUploader` | Project | -| Issues/MR/Notes Legacy Markdown attachments | no | uploads/-/system/note/attachment/:id/:filename | `AttachmentUploader` | Note | -| CI Artifacts (CE) | yes | shared/artifacts/:disk_hash[0..1]/:disk_hash[2..3]/:disk_hash/:year_:month_:date/:job_id/:job_artifact_id (:disk_hash is SHA256 digest of project_id) | `JobArtifactUploader` | Ci::JobArtifact | +| Issues/MR/Notes Markdown attachments | yes | uploads/:project_path_with_namespace/:random_hex/:filename | `FileUploader` | Project | +| Issues/MR/Notes Legacy Markdown attachments | no | uploads/-/system/note/attachment/:id/:filename | `AttachmentUploader` | Note | +| CI Artifacts (CE) | yes | `shared/artifacts/:disk_hash[0..1]/:disk_hash[2..3]/:disk_hash/:year_:month_:date/:job_id/:job_artifact_id` (:disk_hash is SHA256 digest of project_id) | `JobArtifactUploader` | Ci::JobArtifact | | LFS Objects (CE) | yes | shared/lfs-objects/:hex/:hex/:object_hash | `LfsObjectUploader` | LfsObject | | External merge request diffs | yes | shared/external-diffs/merge_request_diffs/mr-:parent_id/diff-:id | `ExternalDiffUploader` | MergeRequestDiff | diff --git a/doc/development/testing_guide/end_to_end/index.md b/doc/development/testing_guide/end_to_end/index.md index 527cd350633..59eb3ecfd7e 100644 --- a/doc/development/testing_guide/end_to_end/index.md +++ b/doc/development/testing_guide/end_to_end/index.md @@ -79,7 +79,7 @@ subgraph gitlab-ce/ee pipeline end subgraph omnibus-gitlab pipeline - A2[<b>`Trigger-docker` stage</b></b><br />`Trigger:gitlab-docker` job] -->|once done| B2 + A2[<b>`Trigger-docker` stage</b><br />`Trigger:gitlab-docker` job] -->|once done| B2 end subgraph gitlab-qa pipeline diff --git a/doc/development/testing_guide/end_to_end/quick_start_guide.md b/doc/development/testing_guide/end_to_end/quick_start_guide.md index 041bdf716b3..064fb0e31dd 100644 --- a/doc/development/testing_guide/end_to_end/quick_start_guide.md +++ b/doc/development/testing_guide/end_to_end/quick_start_guide.md @@ -394,15 +394,15 @@ end By defining the `api_get_path` method, we allow the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to get a single issue. -> This `GET` path can be found in the [public API documentation](https://docs.gitlab.com/ee/api/issues.html#single-issue). +> This `GET` path can be found in the [public API documentation](../../../api/issues.md#single-issue). By defining the `api_post_path` method, we allow the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to create a new issue in a specific project. -> This `POST` path can be found in the [public API documentation](https://docs.gitlab.com/ee/api/issues.html#new-issue). +> This `POST` path can be found in the [public API documentation](../../../api/issues.md#new-issue). By defining the `api_post_body` method, we allow the [`ApiFabricator.api_post`](https://gitlab.com/gitlab-org/gitlab-ee/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/api_fabricator.rb#L68) method to know which data to send when making the `POST` request. -> Notice that we pass both `labels` and `title` attributes in the `api_post_body`, where `labels` receives an array of labels, and [`title` is required](https://docs.gitlab.com/ee/api/issues.html#new-issue). Also, notice that we keep them alphabetically organized. +> Notice that we pass both `labels` and `title` attributes in the `api_post_body`, where `labels` receives an array of labels, and [`title` is required](../../../api/issues.md#new-issue). Also, notice that we keep them alphabetically organized. **Label resource** @@ -441,7 +441,7 @@ By defining the `api_post_path` method, we allow for the [`ApiFabricator `](http By defining the `api_post_body` method, we we allow for the [`ApiFabricator.api_post`](https://gitlab.com/gitlab-org/gitlab-ee/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/api_fabricator.rb#L68) method to know which data to send when making the `POST` request. -> Notice that we pass both `color` and `name` attributes in the `api_post_body` since [those are required](https://docs.gitlab.com/ee/api/labels.html#create-a-new-label). Also, notice that we keep them alphabetically organized. +> Notice that we pass both `color` and `name` attributes in the `api_post_body` since [those are required](../../../api/labels.md#create-a-new-label). Also, notice that we keep them alphabetically organized. ### 8. Page Objects diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index 28ebb6f0f64..98df0b5ea7c 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -560,7 +560,6 @@ end [vue-test]: https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components [rspec]: https://github.com/rspec/rspec-rails#feature-specs [capybara]: https://github.com/teamcapybara/capybara -[karma]: http://karma-runner.github.io/ [jasmine]: https://jasmine.github.io/ --- diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md index 93ee2a6371a..c4b18391cb2 100644 --- a/doc/development/testing_guide/index.md +++ b/doc/development/testing_guide/index.md @@ -80,8 +80,6 @@ Everything you should know about how to run end-to-end tests using [Return to Development documentation](../README.md) -[^1]: /ci/yaml/README.html#dependencies - [rails]: http://rubyonrails.org/ [RSpec]: https://github.com/rspec/rspec-rails#feature-specs [Capybara]: https://github.com/teamcapybara/capybara diff --git a/doc/install/google_cloud_platform/index.md b/doc/install/google_cloud_platform/index.md index 77c61acbfd4..2070187255a 100644 --- a/doc/install/google_cloud_platform/index.md +++ b/doc/install/google_cloud_platform/index.md @@ -128,9 +128,9 @@ GitLab can be configured to authenticate with other OAuth providers, LDAP, SAML, Kerberos, etc. Here are some documents you might be interested in reading: - [Omnibus GitLab documentation](https://docs.gitlab.com/omnibus/) -- [Integration documentation](https://docs.gitlab.com/ce/integration/) -- [GitLab Pages configuration](https://docs.gitlab.com/ce/administration/pages/index.html) -- [GitLab Container Registry configuration](https://docs.gitlab.com/ce/administration/container_registry.html) +- [Integration documentation](../../integration/README.md) +- [GitLab Pages configuration](../../administration/pages/index.md) +- [GitLab Container Registry configuration](../../administration/container_registry.md) [freetrial]: https://console.cloud.google.com/freetrial "GCP free trial" [ip]: https://cloud.google.com/compute/docs/configure-instance-ip-addresses#promote_ephemeral_ip "Configuring an Instance's IP Addresses" diff --git a/doc/install/installation.md b/doc/install/installation.md index 70e5ab28931..e9206469e5d 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -167,7 +167,7 @@ cd pcre2-10.33 chmod +x configure ./configure --prefix=/usr --enable-jit make -make install +sudo make install # Download and compile from source cd /tmp @@ -634,8 +634,8 @@ Gitaly must be running for the next section. gitlab_path=/home/git/gitlab gitaly_path=/home/git/gitaly -sudo -u git -H $gitlab_path/bin/daemon_with_pidfile $gitlab_path/tmp/pids/gitaly.pid \ - $gitaly_path/gitaly $gitaly_path/config.toml >> $gitlab_path/log/gitaly.log 2>&1 & +sudo -u git -H sh -c "$gitlab_path/bin/daemon_with_pidfile $gitlab_path/tmp/pids/gitaly.pid \ + $gitaly_path/gitaly $gitaly_path/config.toml >> $gitlab_path/log/gitaly.log 2>&1 &" ``` ### Initialize Database and Activate Advanced Features diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md index 0729875daf8..b28f21f83a9 100644 --- a/doc/raketasks/README.md +++ b/doc/raketasks/README.md @@ -13,6 +13,6 @@ comments: false - [User management](user_management.md) - [Webhooks](web_hooks.md) - [Import](import.md) of git repositories in bulk -- [Rebuild authorized_keys file](http://docs.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators +- [Rebuild authorized_keys file](../administration/raketasks/maintenance.md#rebuild-authorized_keys-file) task for administrators - [Migrate Uploads](../administration/raketasks/uploads/migrate.md) - [Sanitize Uploads](../administration/raketasks/uploads/sanitize.md) diff --git a/doc/topics/application_development_platform/index.md b/doc/topics/application_development_platform/index.md index 8742606479d..2ea561eb943 100644 --- a/doc/topics/application_development_platform/index.md +++ b/doc/topics/application_development_platform/index.md @@ -9,10 +9,10 @@ The GitLab Application Development Platform aims to: - Reduce and even eliminate the time it takes for an Operations team to provide a full environment for software developers. -- Get developers up and running fast so they can focus on writing +- Get developers up and running fast so they can focus on writing great applications with a robust development feature set. -- Provide best-of-breed security features so that applications developed - with GitLab are not affected by vulnerabilities that may lead to security +- Provide best-of-breed security features so that applications developed + with GitLab are not affected by vulnerabilities that may lead to security problems and unintended use. It is comprised of the following high-level elements: @@ -35,28 +35,28 @@ with various cloud providers. ### Build, test, deploy In order to provide modern DevOps workflows, our Application Development Platform will rely on -[Auto DevOps](https://docs.gitlab.com/ee/topics/autodevops/) to provide those workflows. Auto DevOps works with -any Kubernetes cluster; you're not limited to running on GitLab's infrastructure. Additionally, Auto DevOps offers -an incremental consumption path. Because it is [composable](https://docs.gitlab.com/ee/topics/autodevops/#using-components-of-auto-devops), +[Auto DevOps](../autodevops/index.md) to provide those workflows. Auto DevOps works with +any Kubernetes cluster; you're not limited to running on GitLab's infrastructure. Additionally, Auto DevOps offers +an incremental consumption path. Because it is [composable](../autodevops/index.md#using-components-of-auto-devops), you can use as much or as little of the default pipeline as you'd like, and deeply customize without having to integrate a completely different platform. ### Security -The Application Development Platform helps you ensure that the applications you create are not affected by vulnerabilities +The Application Development Platform helps you ensure that the applications you create are not affected by vulnerabilities that may lead to security problems and unintended use. This can be achieved by making use of the embedded security features of Auto DevOps, -which inform security teams and developers if there is something to consider changing in their apps +which inform security teams and developers if there is something to consider changing in their apps before it is too late to create a preventative fix. The following features are included: -- [Auto SAST (Static Application Security Testing)](https://docs.gitlab.com/ee/topics/autodevops/#auto-sast-ultimate) -- [Auto Dependency Scanning](https://docs.gitlab.com/ee/topics/autodevops/#auto-dependency-scanning-ultimate) -- [Auto Container Scanning](https://docs.gitlab.com/ee/topics/autodevops/#auto-container-scanning-ultimate) -- [Auto DAST (Dynamic Application Security Testing)](https://docs.gitlab.com/ee/topics/autodevops/#auto-dast-ultimate) +- [Auto SAST (Static Application Security Testing)](../autodevops/index.md#auto-sast-ultimate) +- [Auto Dependency Scanning](../autodevops/index.md#auto-dependency-scanning-ultimate) +- [Auto Container Scanning](../autodevops/index.md#auto-container-scanning-ultimate) +- [Auto DAST (Dynamic Application Security Testing)](../autodevops/index.md#auto-dast-ultimate) ### Observability Performance is a critical aspect of the user experience, and ensuring your application is responsive and available is everyone's -responsibility. The Application Development Platform integrates key performance analytics and feedback +responsibility. The Application Development Platform integrates key performance analytics and feedback into GitLab, automatically. The following features are included: -- [Auto Monitoring](https://docs.gitlab.com/ee/topics/autodevops/#auto-monitoring) -- [In-app Kubernetes Pod Logs](https://docs.gitlab.com/ee/user/project/clusters/kubernetes_pod_logs.html)
\ No newline at end of file +- [Auto Monitoring](../autodevops/index.md#auto-monitoring) +- [In-app Kubernetes Pod Logs](../../user/project/clusters/kubernetes_pod_logs.md) diff --git a/doc/university/README.md b/doc/university/README.md index 9d861460618..f696db2df20 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -73,7 +73,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres - Being part of our Great Community and Contributing to GitLab 1. [Getting Started with the GitLab Development Kit (GDK)](https://about.gitlab.com/2016/06/08/getting-started-with-gitlab-development-kit/) 1. [Contributing Technical Articles to the GitLab Blog](https://about.gitlab.com/2016/01/26/call-for-writers/) -1. [GitLab Training Workshops](https://docs.gitlab.com/ce/university/training/end-user/) +1. [GitLab Training Workshops](training/end-user/README.md) 1. [GitLab Professional Services](https://about.gitlab.com/services/) ### 1.8 GitLab Training Material diff --git a/doc/user/application_security/license_management/index.md b/doc/user/application_security/license_management/index.md index 957c4ede981..de131ba1f23 100644 --- a/doc/user/application_security/license_management/index.md +++ b/doc/user/application_security/license_management/index.md @@ -46,12 +46,19 @@ The following languages and package managers are supported. | Language | Package managers | Scan Tool | |------------|-------------------------------------------------------------------|----------------------------------------------------------| -| JavaScript | [Bower](https://bower.io/), [npm](https://www.npmjs.com/) |[License Finder](https://github.com/pivotal/LicenseFinder)| -| Go | [Godep](https://github.com/tools/godep), go get |[License Finder](https://github.com/pivotal/LicenseFinder)| +| JavaScript | [Bower](https://bower.io/), [npm](https://www.npmjs.com/), [yarn](https://yarnpkg.com/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)) |[License Finder](https://github.com/pivotal/LicenseFinder)| +| Go | [Godep](https://github.com/tools/godep), go get ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)), gvt ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)), glide ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)), dep ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)), trash ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)) and govendor ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)), [go mod](https://github.com/golang/go/wiki/Modules) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)) |[License Finder](https://github.com/pivotal/LicenseFinder)| | Java | [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) |[License Finder](https://github.com/pivotal/LicenseFinder)| | .NET | [Nuget](https://www.nuget.org/) |[License Finder](https://github.com/pivotal/LicenseFinder)| | Python | [pip](https://pip.pypa.io/en/stable/) |[License Finder](https://github.com/pivotal/LicenseFinder)| | Ruby | [gem](https://rubygems.org/) |[License Finder](https://github.com/pivotal/LicenseFinder)| +| Erlang | [rebar](https://www.rebar3.org/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types))|[License Finder](https://github.com/pivotal/LicenseFinder)| +| Objective-C, Swift | [Carthage](https://github.com/Carthage/Carthage) , [CocoaPods v0.39 and below](https://cocoapods.org/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)) |[License Finder](https://github.com/pivotal/LicenseFinder)| +| Elixir | [mix](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)) |[License Finder](https://github.com/pivotal/LicenseFinder)| +| C++/C | [conan](https://conan.io/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types))|[License Finder](https://github.com/pivotal/LicenseFinder)| +| Scala | [sbt](https://www.scala-sbt.org/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types))|[License Finder](https://github.com/pivotal/LicenseFinder)| +| Rust | [cargo](https://crates.io/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types))|[License Finder](https://github.com/pivotal/LicenseFinder)| +| PHP | [composer](https://getcomposer.org/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types))|[License Finder](https://github.com/pivotal/LicenseFinder)| ## Requirements diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 886fb6e6f55..769a22315be 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -73,9 +73,9 @@ or over the size limit, you can [reduce your repository size with Git](../projec ## IP range -GitLab.com, CI/CD, and related services are deployed into Google Cloud Platform (GCP). Any -IP based firewall can be configured by looking up all -[IP address ranges or CIDR blocks for GCP](https://cloud.google.com/compute/docs/faq#where_can_i_find_product_name_short_ip_ranges). +GitLab.com, CI/CD, and related services are deployed into Google Cloud Platform (GCP). Any +IP based firewall can be configured by looking up all +[IP address ranges or CIDR blocks for GCP](https://cloud.google.com/compute/docs/faq#where_can_i_find_product_name_short_ip_ranges). [Static endpoints](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/5071) are being considered. @@ -353,12 +353,10 @@ High Performance TCP/HTTP Load Balancer: [4010]: https://gitlab.com/gitlab-com/infrastructure/issues/4010 "Find a good value for maximum timeout for Shared Runners" [4070]: https://gitlab.com/gitlab-com/infrastructure/issues/4070 "Configure per-runner timeout for shared-runners-manager-X on GitLab.com" -## Other admin area settings +## Group and project settings -This area highlights other noteworthy admin area settings on GitLab.com that differ from default settings. This list is not exhaustive. +On GitLab.com, projects, groups, and snippets created +after July 2019 have the `Internal` visibility setting disabled. -NOTE: **Note:** -From July 2019, the `Internal` visibility setting is disabled for new projects, groups, -and snippets on GitLab.com. Existing projects, groups, and snippets using the `Internal` -visibility setting keep this setting. You can read more about the change in the +You can read more about the change in the [relevant issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/12388). diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 04a9d9568ca..30940b65454 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -24,7 +24,7 @@ to the webhook URL. In most cases, you'll need to set up your own [webhook receiver](#example-webhook-receiver) to receive information from GitLab, and send it to another app, according to your needs. -We already have a [built-in receiver](https://docs.gitlab.com/ce/project_services/slack.html) +We already have a [built-in receiver](slack.md) for sending [Slack](https://api.slack.com/incoming-webhooks) notifications _per project_. ## Overview diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md index 91dbf0d848e..1eb835d4606 100644 --- a/doc/user/project/issues/managing_issues.md +++ b/doc/user/project/issues/managing_issues.md @@ -115,7 +115,7 @@ The "Move issue" button is at the bottom of the right-sidebar when viewing the i If you have advanced technical skills you can also bulk move all the issues from one project to another in the rails console. The below script will move all the issues from one project to another that are not in status **closed**. -To access rails console run `sudo gitlab-rails console` on the GitLab server and run the below script. Please be sure to change **project**, **admin_user** and **target_project** to your values. We do also recommend [creating a backup](https://docs.gitlab.com/ee/raketasks/backup_restore.html#creating-a-backup-of-the-gitlab-system) before attempting any changes in the console. +To access rails console run `sudo gitlab-rails console` on the GitLab server and run the below script. Please be sure to change **project**, **admin_user** and **target_project** to your values. We do also recommend [creating a backup](../../../raketasks/backup_restore.md#creating-a-backup-of-the-gitlab-system) before attempting any changes in the console. ```ruby project = Project.find_by_full_path('full path of the project where issues are moved from') diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md index 8baf41dba78..d844d4222b1 100644 --- a/doc/user/project/pages/getting_started_part_four.md +++ b/doc/user/project/pages/getting_started_part_four.md @@ -380,7 +380,7 @@ What you can do with GitLab CI is pretty much up to your creativity. Once you get used to it, you start creating awesome scripts that automate most of tasks you'd do manually in the past. Read through the -[documentation of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) +[documentation of GitLab CI](../../../ci/yaml/README.md) to understand how to go even further on your scripts. - On this blog post, understand the concept of diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md index f9feae36dbc..1966b136e9d 100644 --- a/doc/user/project/pipelines/job_artifacts.md +++ b/doc/user/project/pipelines/job_artifacts.md @@ -187,10 +187,13 @@ information in the UI. DANGER: **Warning:** This is a destructive action that leads to data loss. Use with caution. -If you are either the owner of a given job or have Master -[permissions](../../permissions.md#gitlab-cicd-permissions) -on the project, you can erase a single job via the UI which will also remove the -artifacts and the job's trace. +You can erase a single job via the UI, which will also remove the job's +artifacts and trace, if you are: + +- The owner of the job. +- A [Maintainer](../../permissions.md#gitlab-cicd-permissions) of the project. + +To erase a job: 1. Navigate to a job's page. 1. Click the trash icon at the top right of the job's trace. diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index 2bf8d4dfe7b..01763c49207 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -9,7 +9,7 @@ to your project's homepage and clicking **Settings**. ## General settings -Under a project's general settings you can find everything concerning the +Under a project's general settings, you can find everything concerning the functionality of a project. ### General project settings @@ -26,7 +26,7 @@ Set up your project's access, [visibility](../../../public_access/public_access. ![projects sharing permissions](img/sharing_and_permissions_settings.png) -If Issues are disabled, or you can't access Issues because you're not a project member, then Lables and Milestones +If Issues are disabled, or you can't access Issues because you're not a project member, then Labels and Milestones links will be missing from the sidebar UI. You can still access them with direct links if you can access Merge Requests. This is deliberate, if you can see @@ -96,7 +96,7 @@ To rename a repository: 1. Hit **Rename project**. Remember that this can have unintended side effects since everyone with the -old URL will not be able to push or pull. Read more about what happens with the +old URL will not be able to push or pull. Read more about what happens with the [redirects when renaming repositories](../index.md#redirects-when-changing-repository-paths). #### Transferring an existing project into another namespace diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 0cb390e1242..2f365e42cc9 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -1,7 +1,8 @@ -![GitLab Flow](gitlab_flow.png) # Introduction to GitLab Flow +![GitLab Flow](img/gitlab_flow.png) + Git allows a wide variety of branching strategies and workflows. Because of this, many organizations end up with workflows that are too complicated, not clearly defined, or not integrated with issue tracking systems. Therefore, we propose GitLab flow as a clearly defined set of best practices. @@ -11,7 +12,7 @@ Organizations coming to Git from other version control systems frequently find i This article describes GitLab flow, which integrates the Git workflow with an issue tracking system. It offers a simple, transparent, and effective way to work with Git. -![Four stages (working copy, index, local repo, remote repo) and three steps between them](four_stages.png) +![Four stages (working copy, index, local repo, remote repo) and three steps between them](img/four_stages.png) When converting to Git, you have to get used to the fact that it takes three steps to share a commit with colleagues. Most version control systems have only one step: committing from the working copy to a shared server. @@ -19,7 +20,7 @@ In Git, you add files from the working copy to the staging area. After that, you The third step is pushing to a shared remote repository. After getting used to these three steps, the next challenge is the branching model. -![Multiple long-running branches and merging in all directions](messy_flow.png) +![Multiple long-running branches and merging in all directions](img/messy_flow.png) Since many organizations new to Git have no conventions for how to work with it, their repositories can quickly become messy. The biggest problem is that many long-running branches emerge that all contain part of the changes. @@ -31,7 +32,7 @@ For a video introduction of how this works in GitLab, see [GitLab Flow](https:// ## Git flow and its problems -![Git Flow timeline by Vincent Driessen, used with permission](gitdashflow.png) +![Git Flow timeline by Vincent Driessen, used with permission](img/gitdashflow.png) Git flow was one of the first proposals to use Git branches, and it has received a lot of attention. It suggests a `master` branch and a separate `develop` branch, as well as supporting branches for features, releases, and hotfixes. @@ -54,7 +55,7 @@ For example, many projects do releases but don't need to do hotfixes. ## GitHub flow as a simpler alternative -![Master branch with feature branches merged in](github_flow.png) +![Master branch with feature branches merged in](img/github_flow.png) In reaction to Git flow, GitHub created a simpler alternative. [GitHub flow](https://guides.github.com/introduction/flow/index.html) has only feature branches and a `master` branch. @@ -66,7 +67,7 @@ With GitLab flow, we offer additional guidance for these questions. ## Production branch with GitLab flow -![Master branch and production branch with an arrow that indicates a deployment](production_branch.png) +![Master branch and production branch with an arrow that indicates a deployment](img/production_branch.png) GitHub flow assumes you can deploy to production every time you merge a feature branch. While this is possible in some cases, such as SaaS applications, there are many cases where this is not possible. @@ -82,7 +83,7 @@ This flow prevents the overhead of releasing, tagging, and merging that happens ## Environment branches with GitLab flow -![Multiple branches with the code cascading from one to another](environment_branches.png) +![Multiple branches with the code cascading from one to another](img/environment_branches.png) It might be a good idea to have an environment that is automatically updated to the `master` branch. Only, in this case, the name of this environment might differ from the branch name. @@ -98,7 +99,7 @@ If this is not possible because more manual testing is required, you can send me ## Release branches with GitLab flow -![Master and multiple release branches that vary in length with cherry-picks from master](release_branches.png) +![Master and multiple release branches that vary in length with cherry-picks from master](img/release_branches.png) You only need to work with release branches if you need to release software to the outside world. In this case, each branch contains a minor version, for example, 2-3-stable, 2-4-stable, etc. @@ -114,7 +115,7 @@ In this flow, it is not common to have a production branch (or Git flow `master` ## Merge/pull requests with GitLab flow -![Merge request with inline comments](mr_inline_comments.png) +![Merge request with inline comments](img/mr_inline_comments.png) Merge or pull requests are created in a Git management application. They ask an assigned person to merge two branches. Tools such as GitHub and Bitbucket choose the name "pull request" since the first manual action is to pull the feature branch. @@ -147,11 +148,11 @@ It also ensures that if someone reopens the issue, they can use the same branch NOTE: **Note:** When you reopen an issue you need to create a new merge request. -![Remove checkbox for branch in merge requests](remove_checkbox.png) +![Remove checkbox for branch in merge requests](img/remove_checkbox.png) ## Issue tracking with GitLab flow -![Merge request with the branch name "15-require-a-password-to-change-it" and assignee field shown](merge_request.png) +![Merge request with the branch name "15-require-a-password-to-change-it" and assignee field shown](img/merge_request.png) GitLab flow is a way to make the relation between the code and the issue tracker more transparent. @@ -192,7 +193,7 @@ It is possible that one feature branch solves more than one issue. ## Linking and closing issues from merge requests -![Merge request showing the linked issues that will be closed](close_issue_mr.png) +![Merge request showing the linked issues that will be closed](img/close_issue_mr.png) Link to issues by mentioning them in commit messages or the description of a merge request, for example, "Fixes #16" or "Duck typing is preferred. See #12." GitLab then creates links to the mentioned issues and creates comments in the issues linking back to the merge request. @@ -203,7 +204,7 @@ If you have an issue that spans across multiple repositories, create an issue fo ## Squashing commits with rebase -![Vim screen showing the rebase view](rebase.png) +![Vim screen showing the rebase view](img/rebase.png) With Git, you can use an interactive rebase (`rebase -i`) to squash multiple commits into one or reorder them. This functionality is useful if you want to replace a couple of small commits with a single commit, or if you want to make the order more logical. @@ -229,7 +230,7 @@ Git does not allow you to merge the code again otherwise. ## Reducing merge commits in feature branches -![List of sequential merge commits](merge_commits.png) +![List of sequential merge commits](img/merge_commits.png) Having lots of merge commits can make your repository history messy. Therefore, you should try to avoid merge commits in feature branches. @@ -289,7 +290,7 @@ Sharing your work before it's complete also allows for discussion and feedback a ## How to write a good commit message -![Good and bad commit message](good_commit.png) +![Good and bad commit message](img/good_commit.png) A commit message should reflect your intention, not just the contents of the commit. It is easy to see the changes in a commit, so the commit message should explain why you made those changes. @@ -300,7 +301,7 @@ For more information about formatting commit messages, please see this excellent ## Testing before merging -![Merge requests showing the test states: red, yellow, and green](ci_mr.png) +![Merge requests showing the test states: red, yellow, and green](img/ci_mr.png) In old workflows, the continuous integration (CI) server commonly ran tests on the `master` branch only. Developers had to ensure their code did not break the `master` branch. @@ -317,7 +318,7 @@ As said before, if you often have feature branches that last for more than a few ## Working with feature branches -![Shell output showing git pull output](git_pull.png) +![Shell output showing git pull output](img/git_pull.png) When creating a feature branch, always branch from an up-to-date `master`. If you know before you start that your work depends on another branch, you can also branch from there. diff --git a/doc/workflow/ci_mr.png b/doc/workflow/img/ci_mr.png Binary files differindex 85a609cb814..85a609cb814 100644 --- a/doc/workflow/ci_mr.png +++ b/doc/workflow/img/ci_mr.png diff --git a/doc/workflow/close_issue_mr.png b/doc/workflow/img/close_issue_mr.png Binary files differindex 70de2fb6cee..70de2fb6cee 100644 --- a/doc/workflow/close_issue_mr.png +++ b/doc/workflow/img/close_issue_mr.png diff --git a/doc/workflow/environment_branches.png b/doc/workflow/img/environment_branches.png Binary files differindex 0aff33c6bb8..0aff33c6bb8 100644 --- a/doc/workflow/environment_branches.png +++ b/doc/workflow/img/environment_branches.png diff --git a/doc/workflow/four_stages.png b/doc/workflow/img/four_stages.png Binary files differindex 3ef6a33d2d4..3ef6a33d2d4 100644 --- a/doc/workflow/four_stages.png +++ b/doc/workflow/img/four_stages.png diff --git a/doc/workflow/git_pull.png b/doc/workflow/img/git_pull.png Binary files differindex 0e56e59471c..0e56e59471c 100644 --- a/doc/workflow/git_pull.png +++ b/doc/workflow/img/git_pull.png diff --git a/doc/workflow/gitdashflow.png b/doc/workflow/img/gitdashflow.png Binary files differindex 65900853d84..65900853d84 100644 --- a/doc/workflow/gitdashflow.png +++ b/doc/workflow/img/gitdashflow.png diff --git a/doc/workflow/github_flow.png b/doc/workflow/img/github_flow.png Binary files differindex 21a22becdb6..21a22becdb6 100644 --- a/doc/workflow/github_flow.png +++ b/doc/workflow/img/github_flow.png diff --git a/doc/workflow/gitlab_flow.png b/doc/workflow/img/gitlab_flow.png Binary files differindex a6f3c947843..a6f3c947843 100644 --- a/doc/workflow/gitlab_flow.png +++ b/doc/workflow/img/gitlab_flow.png diff --git a/doc/workflow/good_commit.png b/doc/workflow/img/good_commit.png Binary files differindex ceb0d4b1691..ceb0d4b1691 100644 --- a/doc/workflow/good_commit.png +++ b/doc/workflow/img/good_commit.png diff --git a/doc/workflow/merge_commits.png b/doc/workflow/img/merge_commits.png Binary files differindex 4a80811c6e3..4a80811c6e3 100644 --- a/doc/workflow/merge_commits.png +++ b/doc/workflow/img/merge_commits.png diff --git a/doc/workflow/merge_request.png b/doc/workflow/img/merge_request.png Binary files differindex 010e95983fc..010e95983fc 100644 --- a/doc/workflow/merge_request.png +++ b/doc/workflow/img/merge_request.png diff --git a/doc/workflow/messy_flow.png b/doc/workflow/img/messy_flow.png Binary files differindex 4fa22d2bb5d..4fa22d2bb5d 100644 --- a/doc/workflow/messy_flow.png +++ b/doc/workflow/img/messy_flow.png diff --git a/doc/workflow/mr_inline_comments.png b/doc/workflow/img/mr_inline_comments.png Binary files differindex a18801f56e4..a18801f56e4 100644 --- a/doc/workflow/mr_inline_comments.png +++ b/doc/workflow/img/mr_inline_comments.png diff --git a/doc/workflow/production_branch.png b/doc/workflow/img/production_branch.png Binary files differindex c132d51bfb6..c132d51bfb6 100644 --- a/doc/workflow/production_branch.png +++ b/doc/workflow/img/production_branch.png diff --git a/doc/workflow/rebase.png b/doc/workflow/img/rebase.png Binary files differindex fe865177ba8..fe865177ba8 100644 --- a/doc/workflow/rebase.png +++ b/doc/workflow/img/rebase.png diff --git a/doc/workflow/release_branches.png b/doc/workflow/img/release_branches.png Binary files differindex 0a7f61d0248..0a7f61d0248 100644 --- a/doc/workflow/release_branches.png +++ b/doc/workflow/img/release_branches.png diff --git a/doc/workflow/remove_checkbox.png b/doc/workflow/img/remove_checkbox.png Binary files differindex fb0e792b37b..fb0e792b37b 100644 --- a/doc/workflow/remove_checkbox.png +++ b/doc/workflow/img/remove_checkbox.png diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md index 5a24c254ed1..92e88ea224a 100644 --- a/doc/workflow/repository_mirroring.md +++ b/doc/workflow/repository_mirroring.md @@ -286,10 +286,10 @@ project mirroring again by [Forcing an update](#forcing-an-update-core). [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. Pull mirroring uses polling to detect new branches and commits added upstream, often minutes -afterwards. If you notify GitLab by [API](https://docs.gitlab.com/ee/api/projects.html#start-the-pull-mirroring-process-for-a-project-starter), +afterwards. If you notify GitLab by [API](../api/projects.md#start-the-pull-mirroring-process-for-a-project-starter), updates will be pulled immediately. -For more information, see [Start the pull mirroring process for a Project](https://docs.gitlab.com/ee/api/projects.html#start-the-pull-mirroring-process-for-a-project-starter). +For more information, see [Start the pull mirroring process for a Project](../api/projects.md#start-the-pull-mirroring-process-for-a-project-starter). ## Forcing an update **[CORE]** diff --git a/lib/api/runners.rb b/lib/api/runners.rb index f3fea463e7f..c2d371b6867 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -115,6 +115,8 @@ module API params do requires :id, type: Integer, desc: 'The ID of the runner' optional :status, type: String, desc: 'Status of the job', values: Ci::Build::AVAILABLE_STATUSES + optional :order_by, type: String, desc: 'Order by `id` or not', values: RunnerJobsFinder::ALLOWED_INDEXED_COLUMNS + optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Sort by asc (ascending) or desc (descending)' use :pagination end get ':id/jobs' do diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml index 8713b833011..0a97a16b83c 100644 --- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml @@ -54,6 +54,7 @@ sast: MAVEN_PATH \ MAVEN_REPO_PATH \ SBT_PATH \ + FAIL_NEVER \ ) \ --volume "$PWD:/code" \ --volume /var/run/docker.sock:/var/run/docker.sock \ diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb index d349c378e53..dfa80eb4a64 100644 --- a/lib/gitlab/diff/position.rb +++ b/lib/gitlab/diff/position.rb @@ -134,6 +134,10 @@ module Gitlab @line_code ||= diff_file(repository)&.line_code_for_position(self) end + def file_hash + @file_hash ||= Digest::SHA1.hexdigest(file_path) + end + def on_image? position_type == 'image' end diff --git a/lib/gitlab/diff/position_tracer.rb b/lib/gitlab/diff/position_tracer.rb index af3df820422..a1c82ce9afc 100644 --- a/lib/gitlab/diff/position_tracer.rb +++ b/lib/gitlab/diff/position_tracer.rb @@ -17,187 +17,13 @@ module Gitlab @paths = paths end - def trace(ab_position) + def trace(old_position) return unless old_diff_refs&.complete? && new_diff_refs&.complete? - return unless ab_position.diff_refs == old_diff_refs + return unless old_position.diff_refs == old_diff_refs - # Suppose we have an MR with source branch `feature` and target branch `master`. - # When the MR was created, the head of `master` was commit A, and the - # head of `feature` was commit B, resulting in the original diff A->B. - # Since creation, `master` was updated to C. - # Now `feature` is being updated to D, and the newly generated MR diff is C->D. - # It is possible that C and D are direct descendants of A and B respectively, - # but this isn't necessarily the case as rebases and merges come into play. - # - # Suppose we have a diff note on the original diff A->B. Now that the MR - # is updated, we need to find out what line in C->D corresponds to the - # line the note was originally created on, so that we can update the diff note's - # records and continue to display it in the right place in the diffs. - # If we cannot find this line in the new diff, this means the diff note is now - # outdated, and we will display that fact to the user. - # - # In the new diff, the file the diff note was originally created on may - # have been renamed, deleted or even created, if the file existed in A and B, - # but was removed in C, and restored in D. - # - # Every diff note stores a Position object that defines a specific location, - # identified by paths and line numbers, within a specific diff, identified - # by start, head and base commit ids. - # - # For diff notes for diff A->B, the position looks like this: - # Position - # start_sha - ID of commit A - # head_sha - ID of commit B - # base_sha - ID of base commit of A and B - # old_path - path as of A (nil if file was newly created) - # new_path - path as of B (nil if file was deleted) - # old_line - line number as of A (nil if file was newly created) - # new_line - line number as of B (nil if file was deleted) - # - # We can easily update `start_sha` and `head_sha` to hold the IDs of - # commits C and D, and can trivially determine `base_sha` based on those, - # but need to find the paths and line numbers as of C and D. - # - # If the file was unchanged or newly created in A->B, the path as of D can be found - # by generating diff B->D ("head to head"), finding the diff file with - # `diff_file.old_path == position.new_path`, and taking `diff_file.new_path`. - # The path as of C can be found by taking diff C->D, finding the diff file - # with that same `new_path` and taking `diff_file.old_path`. - # The line number as of D can be found by using the LineMapper on diff B->D - # and providing the line number as of B. - # The line number as of C can be found by using the LineMapper on diff C->D - # and providing the line number as of D. - # - # If the file was deleted in A->B, the path as of C can be found - # by generating diff A->C ("base to base"), finding the diff file with - # `diff_file.old_path == position.old_path`, and taking `diff_file.new_path`. - # The path as of D can be found by taking diff C->D, finding the diff file - # with `old_path` set to that `diff_file.new_path` and taking `diff_file.new_path`. - # The line number as of C can be found by using the LineMapper on diff A->C - # and providing the line number as of A. - # The line number as of D can be found by using the LineMapper on diff C->D - # and providing the line number as of C. + strategy = old_position.on_text? ? LineStrategy : ImageStrategy - if ab_position.added? - trace_added_line(ab_position) - elsif ab_position.removed? - trace_removed_line(ab_position) - else # unchanged - trace_unchanged_line(ab_position) - end - end - - private - - def trace_added_line(ab_position) - b_path = ab_position.new_path - b_line = ab_position.new_line - - bd_diff = bd_diffs.diff_file_with_old_path(b_path) - - d_path = bd_diff&.new_path || b_path - d_line = LineMapper.new(bd_diff).old_to_new(b_line) - - if d_line - cd_diff = cd_diffs.diff_file_with_new_path(d_path) - - c_path = cd_diff&.old_path || d_path - c_line = LineMapper.new(cd_diff).new_to_old(d_line) - - if c_line - # If the line is still in D but also in C, it has turned from an - # added line into an unchanged one. - new_position = position(cd_diff, c_line, d_line) - if valid_position?(new_position) - # If the line is still in the MR, we don't treat this as outdated. - { position: new_position, outdated: false } - else - # If the line is no longer in the MR, we unfortunately cannot show - # the current state on the CD diff, so we treat it as outdated. - ac_diff = ac_diffs.diff_file_with_new_path(c_path) - - { position: position(ac_diff, nil, c_line), outdated: true } - end - else - # If the line is still in D and not in C, it is still added. - { position: position(cd_diff, nil, d_line), outdated: false } - end - else - # If the line is no longer in D, it has been removed from the MR. - { position: position(bd_diff, b_line, nil), outdated: true } - end - end - - def trace_removed_line(ab_position) - a_path = ab_position.old_path - a_line = ab_position.old_line - - ac_diff = ac_diffs.diff_file_with_old_path(a_path) - - c_path = ac_diff&.new_path || a_path - c_line = LineMapper.new(ac_diff).old_to_new(a_line) - - if c_line - cd_diff = cd_diffs.diff_file_with_old_path(c_path) - - d_path = cd_diff&.new_path || c_path - d_line = LineMapper.new(cd_diff).old_to_new(c_line) - - if d_line - # If the line is still in C but also in D, it has turned from a - # removed line into an unchanged one. - bd_diff = bd_diffs.diff_file_with_new_path(d_path) - - { position: position(bd_diff, nil, d_line), outdated: true } - else - # If the line is still in C and not in D, it is still removed. - { position: position(cd_diff, c_line, nil), outdated: false } - end - else - # If the line is no longer in C, it has been removed outside of the MR. - { position: position(ac_diff, a_line, nil), outdated: true } - end - end - - def trace_unchanged_line(ab_position) - a_path = ab_position.old_path - a_line = ab_position.old_line - b_path = ab_position.new_path - b_line = ab_position.new_line - - ac_diff = ac_diffs.diff_file_with_old_path(a_path) - - c_path = ac_diff&.new_path || a_path - c_line = LineMapper.new(ac_diff).old_to_new(a_line) - - bd_diff = bd_diffs.diff_file_with_old_path(b_path) - - d_line = LineMapper.new(bd_diff).old_to_new(b_line) - - cd_diff = cd_diffs.diff_file_with_old_path(c_path) - - if c_line && d_line - # If the line is still in C and D, it is still unchanged. - new_position = position(cd_diff, c_line, d_line) - if valid_position?(new_position) - # If the line is still in the MR, we don't treat this as outdated. - { position: new_position, outdated: false } - else - # If the line is no longer in the MR, we unfortunately cannot show - # the current state on the CD diff or any change on the BD diff, - # so we treat it as outdated. - { position: nil, outdated: true } - end - elsif d_line # && !c_line - # If the line is still in D but no longer in C, it has turned from - # an unchanged line into an added one. - # We don't treat this as outdated since the line is still in the MR. - { position: position(cd_diff, nil, d_line), outdated: false } - else # !d_line && (c_line || !c_line) - # If the line is no longer in D, it has turned from an unchanged line - # into a removed one. - { position: position(bd_diff, b_line, nil), outdated: true } - end + strategy.new(self).trace(old_position) end def ac_diffs @@ -216,18 +42,12 @@ module Gitlab @cd_diffs ||= compare(new_diff_refs.start_sha, new_diff_refs.head_sha) end + private + def compare(start_sha, head_sha, straight: false) compare = CompareService.new(project, head_sha).execute(project, start_sha, straight: straight) compare.diffs(paths: paths, expanded: true) end - - def position(diff_file, old_line, new_line) - Position.new(diff_file: diff_file, old_line: old_line, new_line: new_line) - end - - def valid_position?(position) - !!position.diff_line(project.repository) - end end end end diff --git a/lib/gitlab/diff/position_tracer/base_strategy.rb b/lib/gitlab/diff/position_tracer/base_strategy.rb new file mode 100644 index 00000000000..65049daabf4 --- /dev/null +++ b/lib/gitlab/diff/position_tracer/base_strategy.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Gitlab + module Diff + class PositionTracer + class BaseStrategy + attr_reader :tracer + + delegate \ + :project, + :ac_diffs, + :bd_diffs, + :cd_diffs, + to: :tracer + + def initialize(tracer) + @tracer = tracer + end + + def trace(position) + raise NotImplementedError + end + end + end + end +end diff --git a/lib/gitlab/diff/position_tracer/image_strategy.rb b/lib/gitlab/diff/position_tracer/image_strategy.rb new file mode 100644 index 00000000000..79244a17951 --- /dev/null +++ b/lib/gitlab/diff/position_tracer/image_strategy.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Gitlab + module Diff + class PositionTracer + class ImageStrategy < BaseStrategy + def trace(position) + b_path = position.new_path + + # If file exists in B->D (e.g. updated, renamed, removed), let the + # note become outdated. + bd_diff = bd_diffs.diff_file_with_old_path(b_path) + + return { position: new_position(position, bd_diff), outdated: true } if bd_diff + + # If file still exists in the new diff, update the position. + cd_diff = cd_diffs.diff_file_with_new_path(bd_diff&.new_path || b_path) + + return { position: new_position(position, cd_diff), outdated: false } if cd_diff + + # If file exists in A->C (e.g. rebased and same changes were present + # in target branch), let the note become outdated. + ac_diff = ac_diffs.diff_file_with_old_path(position.old_path) + + return { position: new_position(position, ac_diff), outdated: true } if ac_diff + + # If ever there's a case that the file no longer exists in any diff, + # don't set a change position and let the note become outdated. + # + # This should never happen given the file should exist in one of the + # diffs above. + { outdated: true } + end + + private + + def new_position(position, diff_file) + Position.new( + diff_file: diff_file, + x: position.x, + y: position.y, + width: position.width, + height: position.height, + position_type: position.position_type + ) + end + end + end + end +end diff --git a/lib/gitlab/diff/position_tracer/line_strategy.rb b/lib/gitlab/diff/position_tracer/line_strategy.rb new file mode 100644 index 00000000000..8db0fc6f963 --- /dev/null +++ b/lib/gitlab/diff/position_tracer/line_strategy.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +module Gitlab + module Diff + class PositionTracer + class LineStrategy < BaseStrategy + def trace(position) + # Suppose we have an MR with source branch `feature` and target branch `master`. + # When the MR was created, the head of `master` was commit A, and the + # head of `feature` was commit B, resulting in the original diff A->B. + # Since creation, `master` was updated to C. + # Now `feature` is being updated to D, and the newly generated MR diff is C->D. + # It is possible that C and D are direct descendants of A and B respectively, + # but this isn't necessarily the case as rebases and merges come into play. + # + # Suppose we have a diff note on the original diff A->B. Now that the MR + # is updated, we need to find out what line in C->D corresponds to the + # line the note was originally created on, so that we can update the diff note's + # records and continue to display it in the right place in the diffs. + # If we cannot find this line in the new diff, this means the diff note is now + # outdated, and we will display that fact to the user. + # + # In the new diff, the file the diff note was originally created on may + # have been renamed, deleted or even created, if the file existed in A and B, + # but was removed in C, and restored in D. + # + # Every diff note stores a Position object that defines a specific location, + # identified by paths and line numbers, within a specific diff, identified + # by start, head and base commit ids. + # + # For diff notes for diff A->B, the position looks like this: + # Position + # start_sha - ID of commit A + # head_sha - ID of commit B + # base_sha - ID of base commit of A and B + # old_path - path as of A (nil if file was newly created) + # new_path - path as of B (nil if file was deleted) + # old_line - line number as of A (nil if file was newly created) + # new_line - line number as of B (nil if file was deleted) + # + # We can easily update `start_sha` and `head_sha` to hold the IDs of + # commits C and D, and can trivially determine `base_sha` based on those, + # but need to find the paths and line numbers as of C and D. + # + # If the file was unchanged or newly created in A->B, the path as of D can be found + # by generating diff B->D ("head to head"), finding the diff file with + # `diff_file.old_path == position.new_path`, and taking `diff_file.new_path`. + # The path as of C can be found by taking diff C->D, finding the diff file + # with that same `new_path` and taking `diff_file.old_path`. + # The line number as of D can be found by using the LineMapper on diff B->D + # and providing the line number as of B. + # The line number as of C can be found by using the LineMapper on diff C->D + # and providing the line number as of D. + # + # If the file was deleted in A->B, the path as of C can be found + # by generating diff A->C ("base to base"), finding the diff file with + # `diff_file.old_path == position.old_path`, and taking `diff_file.new_path`. + # The path as of D can be found by taking diff C->D, finding the diff file + # with `old_path` set to that `diff_file.new_path` and taking `diff_file.new_path`. + # The line number as of C can be found by using the LineMapper on diff A->C + # and providing the line number as of A. + # The line number as of D can be found by using the LineMapper on diff C->D + # and providing the line number as of C. + + if position.added? + trace_added_line(position) + elsif position.removed? + trace_removed_line(position) + else # unchanged + trace_unchanged_line(position) + end + end + + private + + def trace_added_line(position) + b_path = position.new_path + b_line = position.new_line + + bd_diff = bd_diffs.diff_file_with_old_path(b_path) + + d_path = bd_diff&.new_path || b_path + d_line = LineMapper.new(bd_diff).old_to_new(b_line) + + if d_line + cd_diff = cd_diffs.diff_file_with_new_path(d_path) + + c_path = cd_diff&.old_path || d_path + c_line = LineMapper.new(cd_diff).new_to_old(d_line) + + if c_line + # If the line is still in D but also in C, it has turned from an + # added line into an unchanged one. + new_position = new_position(cd_diff, c_line, d_line) + if valid_position?(new_position) + # If the line is still in the MR, we don't treat this as outdated. + { position: new_position, outdated: false } + else + # If the line is no longer in the MR, we unfortunately cannot show + # the current state on the CD diff, so we treat it as outdated. + ac_diff = ac_diffs.diff_file_with_new_path(c_path) + + { position: new_position(ac_diff, nil, c_line), outdated: true } + end + else + # If the line is still in D and not in C, it is still added. + { position: new_position(cd_diff, nil, d_line), outdated: false } + end + else + # If the line is no longer in D, it has been removed from the MR. + { position: new_position(bd_diff, b_line, nil), outdated: true } + end + end + + def trace_removed_line(position) + a_path = position.old_path + a_line = position.old_line + + ac_diff = ac_diffs.diff_file_with_old_path(a_path) + + c_path = ac_diff&.new_path || a_path + c_line = LineMapper.new(ac_diff).old_to_new(a_line) + + if c_line + cd_diff = cd_diffs.diff_file_with_old_path(c_path) + + d_path = cd_diff&.new_path || c_path + d_line = LineMapper.new(cd_diff).old_to_new(c_line) + + if d_line + # If the line is still in C but also in D, it has turned from a + # removed line into an unchanged one. + bd_diff = bd_diffs.diff_file_with_new_path(d_path) + + { position: new_position(bd_diff, nil, d_line), outdated: true } + else + # If the line is still in C and not in D, it is still removed. + { position: new_position(cd_diff, c_line, nil), outdated: false } + end + else + # If the line is no longer in C, it has been removed outside of the MR. + { position: new_position(ac_diff, a_line, nil), outdated: true } + end + end + + def trace_unchanged_line(position) + a_path = position.old_path + a_line = position.old_line + b_path = position.new_path + b_line = position.new_line + + ac_diff = ac_diffs.diff_file_with_old_path(a_path) + + c_path = ac_diff&.new_path || a_path + c_line = LineMapper.new(ac_diff).old_to_new(a_line) + + bd_diff = bd_diffs.diff_file_with_old_path(b_path) + + d_line = LineMapper.new(bd_diff).old_to_new(b_line) + + cd_diff = cd_diffs.diff_file_with_old_path(c_path) + + if c_line && d_line + # If the line is still in C and D, it is still unchanged. + new_position = new_position(cd_diff, c_line, d_line) + if valid_position?(new_position) + # If the line is still in the MR, we don't treat this as outdated. + { position: new_position, outdated: false } + else + # If the line is no longer in the MR, we unfortunately cannot show + # the current state on the CD diff or any change on the BD diff, + # so we treat it as outdated. + { position: nil, outdated: true } + end + elsif d_line # && !c_line + # If the line is still in D but no longer in C, it has turned from + # an unchanged line into an added one. + # We don't treat this as outdated since the line is still in the MR. + { position: new_position(cd_diff, nil, d_line), outdated: false } + else # !d_line && (c_line || !c_line) + # If the line is no longer in D, it has turned from an unchanged line + # into a removed one. + { position: new_position(bd_diff, b_line, nil), outdated: true } + end + end + + def new_position(diff_file, old_line, new_line) + Position.new( + diff_file: diff_file, + old_line: old_line, + new_line: new_line + ) + end + + def valid_position?(position) + !!position.diff_line(project.repository) + end + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 19b6aab1c4f..060a29be782 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -536,9 +536,9 @@ module Gitlab tags.find { |tag| tag.name == name } end - def merge_to_ref(user, source_sha, branch, target_ref, message) + def merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref) wrapped_gitaly_errors do - gitaly_operation_client.user_merge_to_ref(user, source_sha, branch, target_ref, message) + gitaly_operation_client.user_merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref) end end diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index b42e6cbad8d..783c2ff0915 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -100,14 +100,15 @@ module Gitlab end end - def user_merge_to_ref(user, source_sha, branch, target_ref, message) + def user_merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref) request = Gitaly::UserMergeToRefRequest.new( repository: @gitaly_repo, source_sha: source_sha, branch: encode_binary(branch), target_ref: encode_binary(target_ref), user: Gitlab::Git::User.from_gitlab(user).to_gitaly, - message: encode_binary(message) + message: encode_binary(message), + first_parent_ref: encode_binary(first_parent_ref) ) response = GitalyClient.call(@repository.storage, :operation_service, :user_merge_to_ref, request) diff --git a/lib/gitlab/graphql/authorize.rb b/lib/gitlab/graphql/authorize.rb index f8d0208e275..e83b567308b 100644 --- a/lib/gitlab/graphql/authorize.rb +++ b/lib/gitlab/graphql/authorize.rb @@ -8,7 +8,7 @@ module Gitlab extend ActiveSupport::Concern def self.use(schema_definition) - schema_definition.instrument(:field, Instrumentation.new, after_built_ins: true) + schema_definition.instrument(:field, Gitlab::Graphql::Authorize::Instrumentation.new, after_built_ins: true) end end end diff --git a/lib/gitlab/graphql/calls_gitaly.rb b/lib/gitlab/graphql/calls_gitaly.rb new file mode 100644 index 00000000000..40cd74a34f2 --- /dev/null +++ b/lib/gitlab/graphql/calls_gitaly.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + # Wraps the field resolution to count Gitaly calls before and after. + # Raises an error if the field calls Gitaly but hadn't declared such. + module CallsGitaly + extend ActiveSupport::Concern + + def self.use(schema_definition) + schema_definition.instrument(:field, Gitlab::Graphql::CallsGitaly::Instrumentation.new, after_built_ins: true) + end + end + end +end diff --git a/lib/gitlab/graphql/calls_gitaly/instrumentation.rb b/lib/gitlab/graphql/calls_gitaly/instrumentation.rb new file mode 100644 index 00000000000..fbd5e348c7d --- /dev/null +++ b/lib/gitlab/graphql/calls_gitaly/instrumentation.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module CallsGitaly + class Instrumentation + # Check if any `calls_gitaly: true` declarations need to be added + # Do nothing if a constant complexity was provided + def instrument(_type, field) + type_object = field.metadata[:type_class] + return field unless type_object.respond_to?(:calls_gitaly?) + return field if type_object.constant_complexity? || type_object.calls_gitaly? + + old_resolver_proc = field.resolve_proc + + gitaly_wrapped_resolve = -> (typed_object, args, ctx) do + previous_gitaly_call_count = Gitlab::GitalyClient.get_request_count + result = old_resolver_proc.call(typed_object, args, ctx) + current_gitaly_call_count = Gitlab::GitalyClient.get_request_count + calls_gitaly_check(type_object, current_gitaly_call_count - previous_gitaly_call_count) + result + end + + field.redefine do + resolve(gitaly_wrapped_resolve) + end + end + + def calls_gitaly_check(type_object, calls) + return if calls < 1 + + # Will inform you if there needs to be `calls_gitaly: true` as a kwarg in the field declaration + # if there is at least 1 Gitaly call involved with the field resolution. + error = RuntimeError.new("Gitaly is called for field '#{type_object.name}' on #{type_object.owner.try(:name)} - please either specify a constant complexity or add `calls_gitaly: true` to the field declaration") + Gitlab::Sentry.track_exception(error) + end + end + end + end +end diff --git a/lib/gitlab/graphql/mount_mutation.rb b/lib/gitlab/graphql/mount_mutation.rb index 9048967d4e1..b10e963170a 100644 --- a/lib/gitlab/graphql/mount_mutation.rb +++ b/lib/gitlab/graphql/mount_mutation.rb @@ -6,11 +6,12 @@ module Gitlab extend ActiveSupport::Concern class_methods do - def mount_mutation(mutation_class) + def mount_mutation(mutation_class, **custom_kwargs) # Using an underscored field name symbol will make `graphql-ruby` # standardize the field name field mutation_class.graphql_name.underscore.to_sym, - mutation: mutation_class + mutation: mutation_class, + **custom_kwargs end end end diff --git a/lib/gitlab/http.rb b/lib/gitlab/http.rb index db2b4dde244..58bce613a98 100644 --- a/lib/gitlab/http.rb +++ b/lib/gitlab/http.rb @@ -10,9 +10,9 @@ module Gitlab RedirectionTooDeep = Class.new(StandardError) HTTP_ERRORS = [ - SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, - Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, - Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError, + SocketError, OpenSSL::SSL::SSLError, OpenSSL::OpenSSLError, + Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, + Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep ].freeze diff --git a/lib/gitlab/namespaced_session_store.rb b/lib/gitlab/namespaced_session_store.rb index 34520078bfb..f0f24c081c3 100644 --- a/lib/gitlab/namespaced_session_store.rb +++ b/lib/gitlab/namespaced_session_store.rb @@ -4,19 +4,24 @@ module Gitlab class NamespacedSessionStore delegate :[], :[]=, to: :store - def initialize(key) + def initialize(key, session = Session.current) @key = key + @session = session end def initiated? - !Session.current.nil? + !session.nil? end def store - return unless Session.current + return unless session - Session.current[@key] ||= {} - Session.current[@key] + session[@key] ||= {} + session[@key] end + + private + + attr_reader :session end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 01bf4949213..78bf48d66fc 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4204,6 +4204,9 @@ msgstr "" msgid "Error fetching diverging counts for branches. Please try again." msgstr "" +msgid "Error fetching forked projects. Please try again." +msgstr "" + msgid "Error fetching labels." msgstr "" @@ -6869,6 +6872,9 @@ msgstr "" msgid "No files found." msgstr "" +msgid "No forks available to you." +msgstr "" + msgid "No job trace" msgstr "" @@ -9242,6 +9248,9 @@ msgstr "" msgid "Select members to invite" msgstr "" +msgid "Select private project" +msgstr "" + msgid "Select project" msgstr "" @@ -10815,6 +10824,9 @@ msgstr "" msgid "This job will automatically run after its timer finishes. Often they are used for incremental roll-out deploys to production environments. When unscheduled it converts into a manual action." msgstr "" +msgid "This may expose confidential information as the selected fork is in another namespace that can have other members." +msgstr "" + msgid "This means you can not push code until you create an empty repository or import existing one." msgstr "" @@ -11155,6 +11167,12 @@ msgstr "" msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed." msgstr "" +msgid "To protect this issues confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private." +msgstr "" + +msgid "To protect this issues confidentiality, a private fork of this project was selected." +msgstr "" + msgid "To see all the user's personal access tokens you must impersonate them first." msgstr "" diff --git a/package.json b/package.json index e645eb8ed1c..47e7dd9c9c9 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@babel/preset-env": "^7.4.4", "@gitlab/csslab": "^1.9.0", "@gitlab/svgs": "^1.66.0", - "@gitlab/ui": "^5.1.0", + "@gitlab/ui": "^5.3.2", "apollo-cache-inmemory": "^1.5.1", "apollo-client": "^2.5.1", "apollo-link": "^1.2.11", diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb index 9c57a0f5afb..51b2af8b4ef 100644 --- a/qa/qa/resource/issue.rb +++ b/qa/qa/resource/issue.rb @@ -16,6 +16,10 @@ module QA attribute :labels attribute :title + def initialize + @labels = [] + end + def fabricate! project.visit! @@ -38,7 +42,7 @@ module QA def api_post_body { - labels: [labels], + labels: labels, title: title } end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb index 23008a58af8..448d4980727 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true module QA - # https://gitlab.com/gitlab-org/quality/staging/issues/40 - context 'Create', :quarantine do + context 'Create' do describe 'Push mirror a repository over HTTP' do it 'configures and syncs a (push) mirrored repository' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/spec/features/issues/user_creates_confidential_merge_request_spec.rb b/spec/features/issues/user_creates_confidential_merge_request_spec.rb new file mode 100644 index 00000000000..7ae4af4667b --- /dev/null +++ b/spec/features/issues/user_creates_confidential_merge_request_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +describe 'User creates confidential merge request on issue page', :js do + include ProjectForksHelper + + let(:user) { create(:user) } + let(:project) { create(:project, :repository, :public) } + let(:issue) { create(:issue, project: project, confidential: true) } + + def visit_confidential_issue + sign_in(user) + visit project_issue_path(project, issue) + wait_for_requests + end + + before do + project.add_developer(user) + end + + context 'user has no private fork' do + before do + fork_project(project, user, repository: true) + visit_confidential_issue + end + + it 'shows that user has no fork available' do + click_button 'Create confidential merge request' + + page.within '.create-confidential-merge-request-dropdown-menu' do + expect(page).to have_content('No forks available to you') + end + end + end + + describe 'user has private fork' do + let(:forked_project) { fork_project(project, user, repository: true) } + + before do + forked_project.update(visibility: Gitlab::VisibilityLevel::PRIVATE) + visit_confidential_issue + end + + it 'create merge request in fork' do + click_button 'Create confidential merge request' + + page.within '.create-confidential-merge-request-dropdown-menu' do + expect(page).to have_button(forked_project.name_with_namespace) + click_button 'Create confidential merge request' + end + + expect(page).to have_content(forked_project.namespace.name) + end + end +end diff --git a/spec/finders/runner_jobs_finder_spec.rb b/spec/finders/runner_jobs_finder_spec.rb index 97304170c4e..01f45a37ba8 100644 --- a/spec/finders/runner_jobs_finder_spec.rb +++ b/spec/finders/runner_jobs_finder_spec.rb @@ -35,5 +35,27 @@ describe RunnerJobsFinder do end end end + + context 'when order_by and sort are specified' do + context 'when order_by id and sort is asc' do + let(:params) { { order_by: 'id', sort: 'asc' } } + let!(:jobs) { create_list(:ci_build, 2, runner: runner, project: project, user: create(:user)) } + + it 'sorts as id: :asc' do + is_expected.to eq(jobs.sort_by(&:id)) + end + end + end + + context 'when order_by is specified and sort is not specified' do + context 'when order_by id and sort is not specified' do + let(:params) { { order_by: 'id' } } + let!(:jobs) { create_list(:ci_build, 2, runner: runner, project: project, user: create(:user)) } + + it 'sorts as id: :desc' do + is_expected.to eq(jobs.sort_by(&:id).reverse) + end + end + end end end diff --git a/spec/frontend/branches/divergence_graph_spec.js b/spec/frontend/branches/divergence_graph_spec.js index 4ed77c3a036..8283bc966e4 100644 --- a/spec/frontend/branches/divergence_graph_spec.js +++ b/spec/frontend/branches/divergence_graph_spec.js @@ -10,12 +10,14 @@ describe('Divergence graph', () => { mock.onGet('/-/diverging_counts').reply(200, { master: { ahead: 1, behind: 1 }, + 'test/hello-world': { ahead: 1, behind: 1 }, }); jest.spyOn(axios, 'get'); document.body.innerHTML = ` - <div class="js-branch-item" data-name="master"></div> + <div class="js-branch-item" data-name="master"><div class="js-branch-divergence-graph"></div></div> + <div class="js-branch-item" data-name="test/hello-world"><div class="js-branch-divergence-graph"></div></div> `; }); @@ -26,7 +28,13 @@ describe('Divergence graph', () => { it('calls axos get with list of branch names', () => init('/-/diverging_counts').then(() => { expect(axios.get).toHaveBeenCalledWith('/-/diverging_counts', { - params: { names: ['master'] }, + params: { names: ['master', 'test/hello-world'] }, }); })); + + it('creates Vue components', () => + init('/-/diverging_counts').then(() => { + expect(document.querySelector('[data-name="master"]').innerHTML).not.toEqual(''); + expect(document.querySelector('[data-name="test/hello-world"]').innerHTML).not.toEqual(''); + })); }); diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap new file mode 100644 index 00000000000..a241c764df7 --- /dev/null +++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Confidential merge request project form group component renders empty state when response is empty 1`] = ` +<div + class="form-group" +> + <label> + Project + </label> + + <div> + <!----> + + <p + class="text-muted mt-1 mb-0" + > + + No forks available to you. + <br /> + + <span> + To protect this issues confidentiality, + <a + class="help-link" + href="https://test.com" + > + fork the project + </a> + and set the forks visiblity to private. + </span> + + <gllink-stub + class="help-link" + href="/help" + target="_blank" + > + <span + class="sr-only" + > + Read more + </span> + + <i + aria-hidden="true" + class="fa fa-question-circle" + /> + </gllink-stub> + </p> + </div> +</div> +`; + +exports[`Confidential merge request project form group component renders fork dropdown 1`] = ` +<div + class="form-group" +> + <label> + Project + </label> + + <div> + <!----> + + <p + class="text-muted mt-1 mb-0" + > + + No forks available to you. + <br /> + + <span> + To protect this issues confidentiality, + <a + class="help-link" + href="https://test.com" + > + fork the project + </a> + and set the forks visiblity to private. + </span> + + <gllink-stub + class="help-link" + href="/help" + target="_blank" + > + <span + class="sr-only" + > + Read more + </span> + + <i + aria-hidden="true" + class="fa fa-question-circle" + /> + </gllink-stub> + </p> + </div> +</div> +`; diff --git a/spec/frontend/confidential_merge_request/components/dropdown_spec.js b/spec/frontend/confidential_merge_request/components/dropdown_spec.js new file mode 100644 index 00000000000..69495f3c161 --- /dev/null +++ b/spec/frontend/confidential_merge_request/components/dropdown_spec.js @@ -0,0 +1,56 @@ +import { mount } from '@vue/test-utils'; +import { GlDropdownItem } from '@gitlab/ui'; +import Dropdown from '~/confidential_merge_request/components/dropdown.vue'; + +let vm; + +function factory(projects = []) { + vm = mount(Dropdown, { + propsData: { + projects, + selectedProject: projects[0], + }, + }); +} + +describe('Confidential merge request project dropdown component', () => { + afterEach(() => { + vm.destroy(); + }); + + it('renders dropdown items', () => { + factory([ + { + id: 1, + name: 'test', + }, + { + id: 2, + name: 'test', + }, + ]); + + expect(vm.findAll(GlDropdownItem).length).toBe(2); + }); + + it('renders selected project icon', () => { + factory([ + { + id: 1, + name: 'test', + }, + { + id: 2, + name: 'test 2', + }, + ]); + + expect(vm.find('.js-active-project-check').classes()).not.toContain('icon'); + expect( + vm + .findAll('.js-active-project-check') + .at(1) + .classes(), + ).toContain('icon'); + }); +}); diff --git a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js new file mode 100644 index 00000000000..3001363f7b9 --- /dev/null +++ b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js @@ -0,0 +1,77 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import ProjectFormGroup from '~/confidential_merge_request/components/project_form_group.vue'; + +const localVue = createLocalVue(); +const mockData = [ + { + id: 1, + name_with_namespace: 'root / gitlab-ce', + path_with_namespace: 'root/gitlab-ce', + namespace: { + full_path: 'root', + }, + }, + { + id: 2, + name_with_namespace: 'test / gitlab-ce', + path_with_namespace: 'test/gitlab-ce', + namespace: { + full_path: 'test', + }, + }, +]; +let vm; +let mock; + +function factory(projects = mockData) { + mock = new MockAdapter(axios); + mock.onGet(/api\/(.*)\/projects\/gitlab-org%2Fgitlab-ce\/forks/).reply(200, projects); + + vm = shallowMount(ProjectFormGroup, { + localVue, + propsData: { + namespacePath: 'gitlab-org', + projectPath: 'gitlab-org/gitlab-ce', + newForkPath: 'https://test.com', + helpPagePath: '/help', + }, + }); +} + +describe('Confidential merge request project form group component', () => { + afterEach(() => { + mock.restore(); + vm.destroy(); + }); + + it('renders fork dropdown', () => { + factory(); + + return localVue.nextTick(() => { + expect(vm.element).toMatchSnapshot(); + }); + }); + + it('sets selected project as first fork', () => { + factory(); + + return localVue.nextTick(() => { + expect(vm.vm.selectedProject).toEqual({ + id: 1, + name: 'root / gitlab-ce', + pathWithNamespace: 'root/gitlab-ce', + namespaceFullpath: 'root', + }); + }); + }); + + it('renders empty state when response is empty', () => { + factory([]); + + return localVue.nextTick(() => { + expect(vm.element).toMatchSnapshot(); + }); + }); +}); diff --git a/spec/javascripts/create_merge_request_dropdown_spec.js b/spec/frontend/create_merge_request_dropdown_spec.js index 00fe3f451f5..6e41fdabdce 100644 --- a/spec/javascripts/create_merge_request_dropdown_spec.js +++ b/spec/frontend/create_merge_request_dropdown_spec.js @@ -1,7 +1,7 @@ import axios from '~/lib/utils/axios_utils'; import MockAdapter from 'axios-mock-adapter'; import CreateMergeRequestDropdown from '~/create_merge_request_dropdown'; -import { TEST_HOST } from 'spec/test_constants'; +import { TEST_HOST } from './helpers/test_constants'; describe('CreateMergeRequestDropdown', () => { let axiosMock; @@ -10,7 +10,7 @@ describe('CreateMergeRequestDropdown', () => { beforeEach(() => { axiosMock = new MockAdapter(axios); - setFixtures(` + document.body.innerHTML = ` <div id="dummy-wrapper-element"> <div class="available"></div> <div class="unavailable"> @@ -18,11 +18,12 @@ describe('CreateMergeRequestDropdown', () => { <div class="text"></div> </div> <div class="js-ref"></div> + <div class="js-create-mr"></div> <div class="js-create-merge-request"></div> <div class="js-create-target"></div> <div class="js-dropdown-toggle"></div> </div> - `); + `; const dummyElement = document.getElementById('dummy-wrapper-element'); dropdown = new CreateMergeRequestDropdown(dummyElement); @@ -36,7 +37,7 @@ describe('CreateMergeRequestDropdown', () => { describe('getRef', () => { it('escapes branch names correctly', done => { const endpoint = `${dropdown.refsPath}contains%23hash`; - spyOn(axios, 'get').and.callThrough(); + jest.spyOn(axios, 'get'); axiosMock.onGet(endpoint).replyOnce({}); dropdown diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb index d36e428a8ee..93b86b9b812 100644 --- a/spec/graphql/gitlab_schema_spec.rb +++ b/spec/graphql/gitlab_schema_spec.rb @@ -21,6 +21,10 @@ describe GitlabSchema do expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Present::Instrumentation)) end + it 'enables using gitaly call checker' do + expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::CallsGitaly::Instrumentation)) + end + it 'has the base mutation' do expect(described_class.mutation).to eq(::Types::MutationType.to_graphql) end diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb index 0d3c3e37daf..77ef8933717 100644 --- a/spec/graphql/types/base_field_spec.rb +++ b/spec/graphql/types/base_field_spec.rb @@ -22,6 +22,24 @@ describe Types::BaseField do expect(field.to_graphql.complexity).to eq 1 end + describe '#base_complexity' do + context 'with no gitaly calls' do + it 'defaults to 1' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true) + + expect(field.base_complexity).to eq 1 + end + end + + context 'with a gitaly call' do + it 'adds 1 to the default value' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true) + + expect(field.base_complexity).to eq 2 + end + end + end + it 'has specified value' do field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, complexity: 12) @@ -52,5 +70,46 @@ describe Types::BaseField do end end end + + context 'calls_gitaly' do + context 'for fields with a resolver' do + it 'adds 1 if true' do + with_gitaly_field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, null: true, calls_gitaly: true) + without_gitaly_field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, null: true) + base_result = without_gitaly_field.to_graphql.complexity.call({}, {}, 2) + + expect(with_gitaly_field.to_graphql.complexity.call({}, {}, 2)).to eq base_result + 1 + end + end + + context 'for fields without a resolver' do + it 'adds 1 if true' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true) + + expect(field.to_graphql.complexity).to eq 2 + end + end + + it 'defaults to false' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true) + + expect(field.base_complexity).to eq Types::BaseField::DEFAULT_COMPLEXITY + end + + context 'with declared constant complexity value' do + it 'has complexity set to that constant' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, complexity: 12) + + expect(field.to_graphql.complexity).to eq 12 + end + + it 'does not raise an error even with Gitaly calls' do + allow(Gitlab::GitalyClient).to receive(:get_request_count).and_return([0, 1]) + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, complexity: 12) + + expect(field.to_graphql.complexity).to eq 12 + end + end + end end end diff --git a/spec/javascripts/monitoring/store/utils_spec.js b/spec/javascripts/monitoring/store/utils_spec.js new file mode 100644 index 00000000000..73dd370ffb3 --- /dev/null +++ b/spec/javascripts/monitoring/store/utils_spec.js @@ -0,0 +1,37 @@ +import { groupQueriesByChartInfo } from '~/monitoring/stores/utils'; + +describe('groupQueriesByChartInfo', () => { + let input; + let output; + + it('groups metrics with the same chart title and y_axis label', () => { + input = [ + { title: 'title', y_label: 'MB', queries: [{}] }, + { title: 'title', y_label: 'MB', queries: [{}] }, + { title: 'new title', y_label: 'MB', queries: [{}] }, + ]; + + output = [ + { title: 'title', y_label: 'MB', queries: [{ metricId: null }, { metricId: null }] }, + { title: 'new title', y_label: 'MB', queries: [{ metricId: null }] }, + ]; + + expect(groupQueriesByChartInfo(input)).toEqual(output); + }); + + // Functionality associated with the /additional_metrics endpoint + it("associates a chart's stringified metric_id with the metric", () => { + input = [{ id: 3, title: 'new title', y_label: 'MB', queries: [{}] }]; + output = [{ id: 3, title: 'new title', y_label: 'MB', queries: [{ metricId: '3' }] }]; + + expect(groupQueriesByChartInfo(input)).toEqual(output); + }); + + // Functionality associated with the /metrics_dashboard endpoint + it('aliases a stringified metrics_id on the metric to the metricId key', () => { + input = [{ title: 'new title', y_label: 'MB', queries: [{ metric_id: 3 }] }]; + output = [{ title: 'new title', y_label: 'MB', queries: [{ metricId: '3', metric_id: 3 }] }]; + + expect(groupQueriesByChartInfo(input)).toEqual(output); + }); +}); diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 65f72a135aa..c97ff5236ec 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import $ from 'jquery'; import _ from 'underscore'; +import Api from '~/api'; import { TEST_HOST } from 'spec/test_constants'; import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; import actionsModule, * as actions from '~/notes/stores/actions'; @@ -8,7 +9,6 @@ import * as mutationTypes from '~/notes/stores/mutation_types'; import * as notesConstants from '~/notes/constants'; import createStore from '~/notes/stores'; import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub'; -import service from '~/notes/services/notes_service'; import testAction from '../../helpers/vuex_action_helper'; import { resetStore } from '../helpers'; import { @@ -846,9 +846,9 @@ describe('Actions Notes Store', () => { let flashContainer; beforeEach(() => { - spyOn(service, 'applySuggestion'); + spyOn(Api, 'applySuggestion'); dispatch.and.returnValue(Promise.resolve()); - service.applySuggestion.and.returnValue(Promise.resolve()); + Api.applySuggestion.and.returnValue(Promise.resolve()); flashContainer = {}; }); @@ -877,7 +877,7 @@ describe('Actions Notes Store', () => { it('when service fails, flashes error message', done => { const response = { response: { data: { message: TEST_ERROR_MESSAGE } } }; - service.applySuggestion.and.returnValue(Promise.reject(response)); + Api.applySuggestion.and.returnValue(Promise.reject(response)); testSubmitSuggestion(done, () => { expect(commit).not.toHaveBeenCalled(); diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index aea02d21048..b755cd1aff0 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -610,4 +610,17 @@ describe Gitlab::Diff::Position do it_behaves_like "diff position json" end end + + describe "#file_hash" do + subject do + described_class.new( + old_path: "image.jpg", + new_path: "image.jpg" + ) + end + + it "returns SHA1 representation of the file_path" do + expect(subject.file_hash).to eq(Digest::SHA1.hexdigest(subject.file_path)) + end + end end diff --git a/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb new file mode 100644 index 00000000000..900816af53a --- /dev/null +++ b/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb @@ -0,0 +1,238 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::PositionTracer::ImageStrategy do + include PositionTracerHelpers + + let(:project) { create(:project, :repository) } + let(:current_user) { project.owner } + let(:file_name) { 'test-file' } + let(:new_file_name) { "#{file_name}-new" } + let(:second_file_name) { "#{file_name}-2" } + let(:branch_name) { 'position-tracer-test' } + let(:old_position) { position(old_path: file_name, new_path: file_name, position_type: 'image') } + + let(:tracer) do + Gitlab::Diff::PositionTracer.new( + project: project, + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs + ) + end + + let(:strategy) { described_class.new(tracer) } + + subject { strategy.trace(old_position) } + + let(:initial_commit) do + project.commit(create_branch(branch_name, 'master')[:branch].name) + end + + describe '#trace' do + describe 'diff scenarios' do + let(:create_file_commit) do + initial_commit + + create_file( + branch_name, + file_name, + Base64.encode64('content') + ) + end + + let(:update_file_commit) do + create_file_commit + + update_file( + branch_name, + file_name, + Base64.encode64('updatedcontent') + ) + end + + let(:update_file_again_commit) do + update_file_commit + + update_file( + branch_name, + file_name, + Base64.encode64('updatedcontentagain') + ) + end + + let(:delete_file_commit) do + create_file_commit + delete_file(branch_name, file_name) + end + + let(:rename_file_commit) do + delete_file_commit + + create_file( + branch_name, + new_file_name, + Base64.encode64('renamedcontent') + ) + end + + let(:create_second_file_commit) do + create_file_commit + + create_file( + branch_name, + second_file_name, + Base64.encode64('morecontent') + ) + end + + let(:create_another_file_commit) do + create_file( + branch_name, + second_file_name, + Base64.encode64('morecontent') + ) + end + + let(:update_another_file_commit) do + update_file( + branch_name, + second_file_name, + Base64.encode64('updatedmorecontent') + ) + end + + context 'when the file was created in the old diff' do + context 'when the file is unchanged between the old and the new diff' do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) } + + it 'returns the new position' do + expect_new_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was updated between the old and the new diff' do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was renamed in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, rename_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, rename_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was removed in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, delete_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file is unchanged in the new diff' do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(create_another_file_commit, update_another_file_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, create_another_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + end + + context 'when the file was changed in the old diff' do + context 'when the file is unchanged in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) } + + it 'returns the new position' do + expect_new_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was updated in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was renamed in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, rename_file_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, rename_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was removed in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, delete_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file is unchanged in the new diff' do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_another_file_commit, update_another_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, create_another_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + end + end + end +end diff --git a/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb new file mode 100644 index 00000000000..7f4902c5b86 --- /dev/null +++ b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb @@ -0,0 +1,1805 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::PositionTracer::LineStrategy do + # Douwe's diary New York City, 2016-06-28 + # -------------------------------------------------------------------------- + # + # Dear diary, + # + # Ideally, we would have a test for every single diff scenario that can + # occur and that the PositionTracer should correctly trace a position + # through, across the following variables: + # + # - Old diff file type: created, changed, renamed, deleted, unchanged (5) + # - Old diff line type: added, removed, unchanged (3) + # - New diff file type: created, changed, renamed, deleted, unchanged (5) + # - New diff line type: added, removed, unchanged (3) + # - Old-to-new diff line change: kept, moved, undone (3) + # + # This adds up to 5 * 3 * 5 * 3 * 3 = 675 different potential scenarios, + # and 675 different tests to cover them all. In reality, it would be fewer, + # since one cannot have a removed line in a created file diff, for example, + # but for the sake of this diary entry, let's be pessimistic. + # + # Writing these tests is a manual and time consuming process, as every test + # requires the manual construction or finding of a combination of diffs that + # create the exact diff scenario we are looking for, and can take between + # 1 and 10 minutes, depending on the farfetchedness of the scenario and + # complexity of creating it. + # + # This means that writing tests to cover all of these scenarios would end up + # taking between 11 and 112 hours in total, which I do not believe is the + # best use of my time. + # + # A better course of action would be to think of scenarios that are likely + # to occur, but also potentially tricky to trace correctly, and only cover + # those, with a few more obvious scenarios thrown in to cover our bases. + # + # Unfortunately, I only came to the above realization once I was about + # 1/5th of the way through the process of writing ALL THE SPECS, having + # already wasted about 3 hours trying to be thorough. + # + # I did find 2 bugs while writing those though, so that's good. + # + # In any case, all of this means that the tests below will be extremely + # (excessively, unjustifiably) thorough for scenarios where "the file was + # created in the old diff" and then drop off to comparatively lackluster + # testing of other scenarios. + # + # I did still try to cover most of the obvious and potentially tricky + # scenarios, though. + + include RepoHelpers + include PositionTracerHelpers + + let(:project) { create(:project, :repository) } + let(:current_user) { project.owner } + let(:repository) { project.repository } + let(:file_name) { "test-file" } + let(:new_file_name) { "#{file_name}-new" } + let(:second_file_name) { "#{file_name}-2" } + let(:branch_name) { "position-tracer-test" } + + let(:old_diff_refs) { raise NotImplementedError } + let(:new_diff_refs) { raise NotImplementedError } + let(:change_diff_refs) { raise NotImplementedError } + let(:old_position) { raise NotImplementedError } + + let(:tracer) do + Gitlab::Diff::PositionTracer.new( + project: project, + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs + ) + end + + let(:strategy) { described_class.new(tracer) } + + subject { strategy.trace(old_position) } + + let(:initial_commit) do + project.commit(create_branch(branch_name, 'master')[:branch].name) + end + + describe "#trace" do + describe "diff scenarios" do + let(:create_file_commit) do + initial_commit + + create_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + B + C + CONTENT + ) + end + + let(:create_second_file_commit) do + create_file_commit + + create_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + E + CONTENT + ) + end + + let(:update_line_commit) do + create_second_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + CONTENT + ) + end + + let(:update_second_file_line_commit) do + update_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + EE + CONTENT + ) + end + + let(:move_line_commit) do + update_second_file_line_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + BB + A + C + CONTENT + ) + end + + let(:add_second_file_line_commit) do + move_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + EE + F + CONTENT + ) + end + + let(:move_second_file_line_commit) do + add_second_file_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + F + EE + CONTENT + ) + end + + let(:delete_line_commit) do + move_second_file_line_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + BB + A + CONTENT + ) + end + + let(:delete_second_file_line_commit) do + delete_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + F + CONTENT + ) + end + + let(:delete_file_commit) do + delete_second_file_line_commit + + delete_file(branch_name, file_name) + end + + let(:rename_file_commit) do + delete_file_commit + + create_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + BB + A + CONTENT + ) + end + + let(:update_line_again_commit) do + rename_file_commit + + update_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + BB + AA + CONTENT + ) + end + + let(:move_line_again_commit) do + update_line_again_commit + + update_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + AA + BB + CONTENT + ) + end + + let(:delete_line_again_commit) do + move_line_again_commit + + update_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + AA + CONTENT + ) + end + + context "when the file was created in the old diff" do + context "when the file is created in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 + A + # 2 + B + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 + BB + # 2 + A + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: 1 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:change_diff_refs) { diff_refs(update_line_commit, delete_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 + A + # 2 + BB + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) + end + end + end + end + end + + context "when the file is changed in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 - A + # 2 1 BB + # 2 + A + # 3 3 C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 - A + # 2 1 BB + # 2 + A + # 3 3 C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: 1 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 1 BB + # 2 2 A + # 3 - C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) + end + end + end + end + end + + context "when the file is renamed in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, rename_file_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 1 BB + # 2 2 A + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 2 + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 1 BB + # 2 - A + # 2 + AA + + it "returns the new position" do + expect_new_position( + old_path: file_name, + new_path: new_file_name, + old_line: old_position.new_line, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, move_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 + AA + # 1 2 BB + # 2 - A + + it "returns the new position" do + expect_new_position( + old_path: file_name, + new_path: new_file_name, + old_line: 1, + new_line: 2 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } + let(:change_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 1 BB + # 2 - A + # 2 + AA + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: new_file_name, + old_line: 2, + new_line: nil + ) + end + end + end + end + end + + context "when the file is deleted in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # 1 - BB + # 2 - A + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 - BB + # 2 - A + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 - BB + # 2 - A + # 3 - C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 - A + # 2 - BB + # 3 - C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 - BB + # 2 - A + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) + end + end + end + end + end + + context "when the file is unchanged in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 2 B + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 2 + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 2 BB + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, move_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 1 BB + # 2 2 A + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 2 BB + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_second_file_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 1 BB + # 2 2 A + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) + end + end + end + end + end + end + + context "when the file was changed in the old diff" do + context "when the file is created in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + BB + # 2 + A + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + BB + # 2 + A + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was changed or deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, create_file_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + A + # 2 + B + # 3 + C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 1, + new_line: nil + ) + end + end + end + end + + context "when the position pointed at a deleted line in the old diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, initial_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when the position pointed at an unchanged line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 1) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 2) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + BB + # 2 + A + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2, new_line: 2) } + + # old diff: + # 1 1 BB + # 2 2 A + # 3 - C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was changed or deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 3, new_line: 3) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + A + # 2 + B + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) + end + end + end + end + end + + context "when the file is changed in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 1 BB + # 2 2 A + # 3 - C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: 1, + new_line: 1 + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 - A + # 2 1 BB + # 2 + A + # 3 3 C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: 2, + new_line: 1 + ) + end + end + + context "when that line was changed or deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 1, + new_line: nil + ) + end + end + end + end + + context "when the position pointed at a deleted line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: old_position.old_line, + new_line: nil + ) + end + end + end + end + end + end + + describe "typical use scenarios" do + let(:second_branch_name) { "#{branch_name}-2" } + + def expect_new_positions(old_attrs, new_attrs) + old_positions = old_attrs.map do |old_attrs| + position(old_attrs) + end + + new_positions = old_positions.map do |old_position| + strategy.trace(old_position) + end + + aggregate_failures do + new_positions.zip(new_attrs).each do |new_position, new_attrs| + if new_attrs&.delete(:change) + expect_change_position(new_attrs, new_position) + else + expect_new_position(new_attrs, new_position) + end + end + end + end + + let(:create_file_commit) do + initial_commit + + create_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + B + C + D + E + F + CONTENT + ) + end + + let(:second_create_file_commit) do + create_file_commit + + create_branch(second_branch_name, branch_name) + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + Z + Z + Z + A + B + C + D + E + F + CONTENT + ) + end + + let(:update_file_commit) do + second_create_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + C + DD + E + F + G + CONTENT + ) + end + + let(:update_file_again_commit) do + update_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + D + E + FF + G + CONTENT + ) + end + + describe "simple push of new commit" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 3 2 C + # 4 - D + # 3 + DD + # 5 4 E + # 6 5 F + # 6 + G + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C + { old_path: file_name, old_line: 4 }, # - D + { new_path: file_name, new_line: 3 }, # + DD + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E + { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F + { new_path: file_name, new_line: 6 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2 }, + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, + { new_path: file_name, new_line: 4, change: true }, + { new_path: file_name, old_line: 3, change: true }, + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, + { new_path: file_name, old_line: 5, change: true }, + { new_path: file_name, new_line: 7 } + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + + describe "force push to overwrite last commit" do + let(:second_create_file_commit) do + create_file_commit + + create_branch(second_branch_name, branch_name) + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + D + E + FF + G + CONTENT + ) + end + + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, second_create_file_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, second_create_file_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 3 2 C + # 4 - D + # 3 + DD + # 5 4 E + # 6 5 F + # 6 + G + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C + { old_path: file_name, old_line: 4 }, # - D + { new_path: file_name, new_line: 3 }, # + DD + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E + { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F + { new_path: file_name, new_line: 6 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2 }, + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, + { new_path: file_name, new_line: 4, change: true }, + { old_path: file_name, old_line: 3, change: true }, + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, + { old_path: file_name, old_line: 5, change: true }, + { new_path: file_name, new_line: 7 } + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + + describe "force push to delete last commit" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:change_diff_refs) { diff_refs(update_file_again_commit, update_file_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 1 A + # 2 - B + # 3 2 C + # 4 - D + # 3 + DD + # 5 4 E + # 6 5 F + # 6 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2 }, + { old_path: file_name, old_line: 2, change: true }, + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, + { old_path: file_name, old_line: 4, change: true }, + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, + { new_path: file_name, new_line: 5, change: true }, + { old_path: file_name, old_line: 6, change: true }, + { new_path: file_name, new_line: 6 } + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + + describe "rebase on top of target branch" do + let(:second_update_file_commit) do + update_file_commit + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + Z + Z + Z + A + C + DD + E + F + G + CONTENT + ) + end + + let(:update_file_again_commit) do + second_update_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + D + E + FF + G + CONTENT + ) + end + + let(:overwrite_update_file_again_commit) do + update_file_again_commit + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + Z + Z + Z + A + BB + C + D + E + FF + G + CONTENT + ) + end + + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, overwrite_update_file_again_commit) } + let(:change_diff_refs) { diff_refs(update_file_again_commit, overwrite_update_file_again_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 + Z + # 2 + Z + # 3 + Z + # 1 4 A + # 2 - B + # 5 + BB + # 3 6 C + # 4 7 D + # 5 8 E + # 6 - F + # 9 + FF + # 0 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 5 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 9 }, # + FF + { new_path: file_name, new_line: 10 } # + G + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + + describe "merge of target branch" do + let(:merge_commit) do + second_create_file_commit + + merge_request = create(:merge_request, source_branch: second_branch_name, target_branch: branch_name, source_project: project) + + repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches") + + project.commit(branch_name) + end + + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, merge_commit) } + let(:change_diff_refs) { diff_refs(update_file_again_commit, merge_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 + Z + # 2 + Z + # 3 + Z + # 1 4 A + # 2 - B + # 5 + BB + # 3 6 C + # 4 7 D + # 5 8 E + # 6 - F + # 9 + FF + # 0 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 5 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 9 }, # + FF + { new_path: file_name, new_line: 10 } # + G + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + + describe "changing target branch" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 1 A + # 2 + BB + # 2 3 C + # 3 - DD + # 4 + D + # 4 5 E + # 5 - F + # 6 + FF + # 7 G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2, change: true }, + { new_path: file_name, new_line: 2 }, + { old_path: file_name, new_path: file_name, old_line: 2, new_line: 3 }, + { new_path: file_name, new_line: 4 }, + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 5 }, + { old_path: file_name, old_line: 5 }, + { new_path: file_name, new_line: 6 }, + { new_path: file_name, new_line: 7 } + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + end + end +end diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index 866550753a8..79b33d4d276 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -1,1896 +1,98 @@ require 'spec_helper' describe Gitlab::Diff::PositionTracer do - # Douwe's diary New York City, 2016-06-28 - # -------------------------------------------------------------------------- - # - # Dear diary, - # - # Ideally, we would have a test for every single diff scenario that can - # occur and that the PositionTracer should correctly trace a position - # through, across the following variables: - # - # - Old diff file type: created, changed, renamed, deleted, unchanged (5) - # - Old diff line type: added, removed, unchanged (3) - # - New diff file type: created, changed, renamed, deleted, unchanged (5) - # - New diff line type: added, removed, unchanged (3) - # - Old-to-new diff line change: kept, moved, undone (3) - # - # This adds up to 5 * 3 * 5 * 3 * 3 = 675 different potential scenarios, - # and 675 different tests to cover them all. In reality, it would be fewer, - # since one cannot have a removed line in a created file diff, for example, - # but for the sake of this diary entry, let's be pessimistic. - # - # Writing these tests is a manual and time consuming process, as every test - # requires the manual construction or finding of a combination of diffs that - # create the exact diff scenario we are looking for, and can take between - # 1 and 10 minutes, depending on the farfetchedness of the scenario and - # complexity of creating it. - # - # This means that writing tests to cover all of these scenarios would end up - # taking between 11 and 112 hours in total, which I do not believe is the - # best use of my time. - # - # A better course of action would be to think of scenarios that are likely - # to occur, but also potentially tricky to trace correctly, and only cover - # those, with a few more obvious scenarios thrown in to cover our bases. - # - # Unfortunately, I only came to the above realization once I was about - # 1/5th of the way through the process of writing ALL THE SPECS, having - # already wasted about 3 hours trying to be thorough. - # - # I did find 2 bugs while writing those though, so that's good. - # - # In any case, all of this means that the tests below will be extremely - # (excessively, unjustifiably) thorough for scenarios where "the file was - # created in the old diff" and then drop off to comparatively lackluster - # testing of other scenarios. - # - # I did still try to cover most of the obvious and potentially tricky - # scenarios, though. + include PositionTracerHelpers - include RepoHelpers - - let(:project) { create(:project, :repository) } - let(:current_user) { project.owner } - let(:repository) { project.repository } - let(:file_name) { "test-file" } - let(:new_file_name) { "#{file_name}-new" } - let(:second_file_name) { "#{file_name}-2" } - let(:branch_name) { "position-tracer-test" } - - let(:old_diff_refs) { raise NotImplementedError } - let(:new_diff_refs) { raise NotImplementedError } - let(:change_diff_refs) { raise NotImplementedError } - let(:old_position) { raise NotImplementedError } - - let(:position_tracer) { described_class.new(project: project, old_diff_refs: old_diff_refs, new_diff_refs: new_diff_refs) } - subject { position_tracer.trace(old_position) } - - def diff_refs(base_commit, head_commit) - Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id) - end - - def text_position_attrs - [:old_line, :new_line] - end - - def position(attrs = {}) - attrs.reverse_merge!( - diff_refs: old_diff_refs + subject do + described_class.new( + project: project, + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs ) - Gitlab::Diff::Position.new(attrs) end - def expect_new_position(attrs, result = subject) - aggregate_failures("expect new position #{attrs.inspect}") do - if attrs.nil? - expect(result[:outdated]).to be_truthy - else - expect(result[:outdated]).to be_falsey + describe '#trace' do + let(:diff_refs) { double(complete?: true) } + let(:project) { double } + let(:old_diff_refs) { diff_refs } + let(:new_diff_refs) { diff_refs } + let(:position) { double(on_text?: on_text?, diff_refs: diff_refs) } + let(:tracer) { double } - new_position = result[:position] - expect(new_position).not_to be_nil + context 'position is on text' do + let(:on_text?) { true } - expect(new_position.diff_refs).to eq(new_diff_refs) + it 'calls LineStrategy#trace' do + expect(Gitlab::Diff::PositionTracer::LineStrategy) + .to receive(:new) + .with(subject) + .and_return(tracer) + expect(tracer).to receive(:trace).with(position) - attrs.each do |attr, value| - if text_position_attrs.include?(attr) - expect(new_position.formatter.send(attr)).to eq(value) - else - expect(new_position.send(attr)).to eq(value) - end - end + subject.trace(position) end end - end - - def expect_change_position(attrs, result = subject) - aggregate_failures("expect change position #{attrs.inspect}") do - expect(result[:outdated]).to be_truthy - - change_position = result[:position] - if attrs.nil? || attrs.empty? - expect(change_position).to be_nil - else - expect(change_position).not_to be_nil - - expect(change_position.diff_refs).to eq(change_diff_refs) - - attrs.each do |attr, value| - if text_position_attrs.include?(attr) - expect(change_position.formatter.send(attr)).to eq(value) - else - expect(change_position.send(attr)).to eq(value) - end - end - end - end - end - - def create_branch(new_name, branch_name) - CreateBranchService.new(project, current_user).execute(new_name, branch_name) - end - - def create_file(branch_name, file_name, content) - Files::CreateService.new( - project, - current_user, - start_branch: branch_name, - branch_name: branch_name, - commit_message: "Create file", - file_path: file_name, - file_content: content - ).execute - project.commit(branch_name) - end - - def update_file(branch_name, file_name, content) - Files::UpdateService.new( - project, - current_user, - start_branch: branch_name, - branch_name: branch_name, - commit_message: "Update file", - file_path: file_name, - file_content: content - ).execute - project.commit(branch_name) - end - - def delete_file(branch_name, file_name) - Files::DeleteService.new( - project, - current_user, - start_branch: branch_name, - branch_name: branch_name, - commit_message: "Delete file", - file_path: file_name - ).execute - project.commit(branch_name) - end - - let(:initial_commit) do - create_branch(branch_name, "master")[:branch].name - project.commit(branch_name) - end - - describe "#trace" do - describe "diff scenarios" do - let(:create_file_commit) do - initial_commit - - create_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - B - C - CONTENT - ) - end - - let(:create_second_file_commit) do - create_file_commit - - create_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - E - CONTENT - ) - end - - let(:update_line_commit) do - create_second_file_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - BB - C - CONTENT - ) - end - - let(:update_second_file_line_commit) do - update_line_commit - - update_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - EE - CONTENT - ) - end - - let(:move_line_commit) do - update_second_file_line_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - BB - A - C - CONTENT - ) - end - - let(:add_second_file_line_commit) do - move_line_commit - - update_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - EE - F - CONTENT - ) - end - - let(:move_second_file_line_commit) do - add_second_file_line_commit - - update_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - F - EE - CONTENT - ) - end - - let(:delete_line_commit) do - move_second_file_line_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - BB - A - CONTENT - ) - end - - let(:delete_second_file_line_commit) do - delete_line_commit - - update_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - F - CONTENT - ) - end - - let(:delete_file_commit) do - delete_second_file_line_commit - - delete_file(branch_name, file_name) - end - - let(:rename_file_commit) do - delete_file_commit - - create_file( - branch_name, - new_file_name, - <<-CONTENT.strip_heredoc - BB - A - CONTENT - ) - end - - let(:update_line_again_commit) do - rename_file_commit - - update_file( - branch_name, - new_file_name, - <<-CONTENT.strip_heredoc - BB - AA - CONTENT - ) - end - - let(:move_line_again_commit) do - update_line_again_commit - - update_file( - branch_name, - new_file_name, - <<-CONTENT.strip_heredoc - AA - BB - CONTENT - ) - end - - let(:delete_line_again_commit) do - move_line_again_commit - - update_file( - branch_name, - new_file_name, - <<-CONTENT.strip_heredoc - AA - CONTENT - ) - end - - context "when the file was created in the old diff" do - context "when the file is created in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 + A - # 2 + B - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: old_position.new_line - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: old_position.new_line - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 + BB - # 2 + A - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: 1 - ) - end - end - - context "when that line was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:change_diff_refs) { diff_refs(update_line_commit, delete_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 3) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 + A - # 2 + BB - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end - - context "when the file is changed in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: old_position.new_line - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 3) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 - A - # 2 1 BB - # 2 + A - # 3 3 C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: old_position.new_line - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 - A - # 2 1 BB - # 2 + A - # 3 3 C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: 1 - ) - end - end - - context "when that line was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 3) } - - # old diff: - # 1 + BB - # 2 + A - # 3 + C - # - # new diff: - # 1 1 BB - # 2 2 A - # 3 - C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end - - context "when the file is renamed in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, rename_file_commit) } - let(:change_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # file_name -> new_file_name - # 1 1 BB - # 2 2 A - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: nil, - new_line: 2 - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # file_name -> new_file_name - # 1 1 BB - # 2 - A - # 2 + AA - - it "returns the new position" do - expect_new_position( - old_path: file_name, - new_path: new_file_name, - old_line: old_position.new_line, - new_line: old_position.new_line - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, move_line_again_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # file_name -> new_file_name - # 1 + AA - # 1 2 BB - # 2 - A - - it "returns the new position" do - expect_new_position( - old_path: file_name, - new_path: new_file_name, - old_line: 1, - new_line: 2 - ) - end - end - - context "when that line was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } - let(:change_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # file_name -> new_file_name - # 1 1 BB - # 2 - A - # 2 + AA - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: new_file_name, - old_line: 2, - new_line: nil - ) - end - end - end - end - end - - context "when the file is deleted in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # 1 - BB - # 2 - A - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + BB - # 2 + A - # 3 + C - # - # new diff: - # 1 - BB - # 2 - A - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 - BB - # 2 - A - # 3 - C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, delete_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 - A - # 2 - BB - # 3 - C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 3) } - # old diff: - # 1 + BB - # 2 + A - # 3 + C - # - # new diff: - # 1 - BB - # 2 - A + context 'position is not on text' do + let(:on_text?) { false } - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end + it 'calls ImageStrategy#trace' do + expect(Gitlab::Diff::PositionTracer::ImageStrategy) + .to receive(:new) + .with(subject) + .and_return(tracer) + expect(tracer).to receive(:trace).with(position) - context "when the file is unchanged in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) } - let(:change_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 1 A - # 2 2 B - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: nil, - new_line: 2 - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } - let(:change_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 1 A - # 2 2 BB - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: nil, - new_line: 1 - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(move_line_commit, move_second_file_line_commit) } - let(:change_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 1 BB - # 2 2 A - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: nil, - new_line: 1 - ) - end - end - - context "when that line was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 1 A - # 2 2 BB - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, delete_second_file_line_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, delete_second_file_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 3) } - - # old diff: - # 1 + BB - # 2 + A - # 3 + C - # - # new diff: - # 1 1 BB - # 2 2 A - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end - end - - context "when the file was changed in the old diff" do - context "when the file is created in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: old_position.new_line - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 + BB - # 2 + A - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: old_position.new_line - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 + BB - # 2 + A - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: 1 - ) - end - end - - context "when that line was changed or deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, create_file_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 + A - # 2 + B - # 3 + C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 1, - new_line: nil - ) - end - end - end - end - - context "when the position pointed at a deleted line in the old diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, initial_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when the position pointed at an unchanged line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 1) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: old_position.new_line - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 2) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 + BB - # 2 + A - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: old_position.new_line - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2, new_line: 2) } - - # old diff: - # 1 1 BB - # 2 2 A - # 3 - C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: 1 - ) - end - end - - context "when that line was changed or deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 3, new_line: 3) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 + A - # 2 + B - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end - - context "when the file is changed in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - - it "returns the new position" do - expect_new_position( - old_path: old_position.old_path, - new_path: old_position.new_path, - old_line: nil, - new_line: old_position.new_line - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 1 BB - # 2 2 A - # 3 - C - - it "returns the new position" do - expect_new_position( - old_path: old_position.old_path, - new_path: old_position.new_path, - old_line: 1, - new_line: 1 - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 - A - # 2 1 BB - # 2 + A - # 3 3 C - - it "returns the new position" do - expect_new_position( - old_path: old_position.old_path, - new_path: old_position.new_path, - old_line: 2, - new_line: 1 - ) - end - end - - context "when that line was changed or deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, update_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 1, - new_line: nil - ) - end - end - end - end - - context "when the position pointed at a deleted line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - - it "returns the new position" do - expect_new_position( - old_path: old_position.old_path, - new_path: old_position.new_path, - old_line: old_position.old_line, - new_line: nil - ) - end - end - end - end + subject.trace(position) end end end - describe "typical use scenarios" do - let(:second_branch_name) { "#{branch_name}-2" } - - def expect_new_positions(old_attrs, new_attrs) - old_positions = old_attrs.map do |old_attrs| - position(old_attrs) - end + describe 'diffs methods' do + let(:project) { create(:project, :repository) } + let(:current_user) { project.owner } - new_positions = old_positions.map do |old_position| - position_tracer.trace(old_position) - end - - aggregate_failures do - new_positions.zip(new_attrs).each do |new_position, new_attrs| - if new_attrs&.delete(:change) - expect_change_position(new_attrs, new_position) - else - expect_new_position(new_attrs, new_position) - end - end - end - end - - let(:create_file_commit) do - initial_commit - - create_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - B - C - D - E - F - CONTENT - ) - end - - let(:second_create_file_commit) do - create_file_commit - - create_branch(second_branch_name, branch_name) - - update_file( - second_branch_name, - file_name, - <<-CONTENT.strip_heredoc - Z - Z - Z - A - B - C - D - E - F - CONTENT + let(:old_diff_refs) do + diff_refs( + project.commit(create_branch('new-branch', 'master')[:branch].name), + create_file('new-branch', 'file.md', 'content') ) end - let(:update_file_commit) do - second_create_file_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - C - DD - E - F - G - CONTENT - ) - end - - let(:update_file_again_commit) do - update_file_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - BB - C - D - E - FF - G - CONTENT + let(:new_diff_refs) do + diff_refs( + create_file('new-branch', 'file.md', 'content'), + update_file('new-branch', 'file.md', 'updatedcontent') ) end - describe "simple push of new commit" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } - let(:change_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 3 2 C - # 4 - D - # 3 + DD - # 5 4 E - # 6 5 F - # 6 + G - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C - { old_path: file_name, old_line: 4 }, # - D - { new_path: file_name, new_line: 3 }, # + DD - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E - { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F - { new_path: file_name, new_line: 6 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, - { old_path: file_name, old_line: 2 }, - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, - { new_path: file_name, new_line: 4, change: true }, - { new_path: file_name, old_line: 3, change: true }, - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, - { new_path: file_name, old_line: 5, change: true }, - { new_path: file_name, new_line: 7 } - ] + describe '#ac_diffs' do + it 'returns the diffs between the base of old and new diff' do + diff_refs = subject.ac_diffs.diff_refs - expect_new_positions(old_position_attrs, new_position_attrs) + expect(diff_refs.base_sha).to eq(old_diff_refs.base_sha) + expect(diff_refs.start_sha).to eq(old_diff_refs.base_sha) + expect(diff_refs.head_sha).to eq(new_diff_refs.base_sha) end end - describe "force push to overwrite last commit" do - let(:second_create_file_commit) do - create_file_commit + describe '#bd_diffs' do + it 'returns the diffs between the HEAD of old and new diff' do + diff_refs = subject.bd_diffs.diff_refs - create_branch(second_branch_name, branch_name) - - update_file( - second_branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - BB - C - D - E - FF - G - CONTENT - ) - end - - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, second_create_file_commit) } - let(:change_diff_refs) { diff_refs(update_file_commit, second_create_file_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 3 2 C - # 4 - D - # 3 + DD - # 5 4 E - # 6 5 F - # 6 + G - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C - { old_path: file_name, old_line: 4 }, # - D - { new_path: file_name, new_line: 3 }, # + DD - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E - { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F - { new_path: file_name, new_line: 6 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, - { old_path: file_name, old_line: 2 }, - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, - { new_path: file_name, new_line: 4, change: true }, - { old_path: file_name, old_line: 3, change: true }, - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, - { old_path: file_name, old_line: 5, change: true }, - { new_path: file_name, new_line: 7 } - ] - - expect_new_positions(old_position_attrs, new_position_attrs) + expect(diff_refs.base_sha).to eq(old_diff_refs.head_sha) + expect(diff_refs.start_sha).to eq(old_diff_refs.head_sha) + expect(diff_refs.head_sha).to eq(new_diff_refs.head_sha) end end - describe "force push to delete last commit" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_file_commit) } - let(:change_diff_refs) { diff_refs(update_file_again_commit, update_file_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - # - # new diff: - # 1 1 A - # 2 - B - # 3 2 C - # 4 - D - # 3 + DD - # 5 4 E - # 6 5 F - # 6 + G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 2 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 6 }, # + FF - { new_path: file_name, new_line: 7 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, - { old_path: file_name, old_line: 2 }, - { old_path: file_name, old_line: 2, change: true }, - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, - { old_path: file_name, old_line: 4, change: true }, - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, - { new_path: file_name, new_line: 5, change: true }, - { old_path: file_name, old_line: 6, change: true }, - { new_path: file_name, new_line: 6 } - ] - - expect_new_positions(old_position_attrs, new_position_attrs) - end - end - - describe "rebase on top of target branch" do - let(:second_update_file_commit) do - update_file_commit - - update_file( - second_branch_name, - file_name, - <<-CONTENT.strip_heredoc - Z - Z - Z - A - C - DD - E - F - G - CONTENT - ) - end - - let(:update_file_again_commit) do - second_update_file_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - BB - C - D - E - FF - G - CONTENT - ) - end - - let(:overwrite_update_file_again_commit) do - update_file_again_commit - - update_file( - second_branch_name, - file_name, - <<-CONTENT.strip_heredoc - Z - Z - Z - A - BB - C - D - E - FF - G - CONTENT - ) - end - - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, overwrite_update_file_again_commit) } - let(:change_diff_refs) { diff_refs(update_file_again_commit, overwrite_update_file_again_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - # - # new diff: - # 1 + Z - # 2 + Z - # 3 + Z - # 1 4 A - # 2 - B - # 5 + BB - # 3 6 C - # 4 7 D - # 5 8 E - # 6 - F - # 9 + FF - # 0 + G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 2 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 6 }, # + FF - { new_path: file_name, new_line: 7 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 5 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 9 }, # + FF - { new_path: file_name, new_line: 10 }, # + G - ] - - expect_new_positions(old_position_attrs, new_position_attrs) - end - end - - describe "merge of target branch" do - let(:merge_commit) do - second_create_file_commit - - merge_request = create(:merge_request, source_branch: second_branch_name, target_branch: branch_name, source_project: project) - - repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches") - - project.commit(branch_name) - end - - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, merge_commit) } - let(:change_diff_refs) { diff_refs(update_file_again_commit, merge_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - # - # new diff: - # 1 + Z - # 2 + Z - # 3 + Z - # 1 4 A - # 2 - B - # 5 + BB - # 3 6 C - # 4 7 D - # 5 8 E - # 6 - F - # 9 + FF - # 0 + G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 2 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 6 }, # + FF - { new_path: file_name, new_line: 7 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 5 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 9 }, # + FF - { new_path: file_name, new_line: 10 }, # + G - ] - - expect_new_positions(old_position_attrs, new_position_attrs) - end - end - - describe "changing target branch" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } - let(:new_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, update_file_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - # - # new diff: - # 1 1 A - # 2 + BB - # 2 3 C - # 3 - DD - # 4 + D - # 4 5 E - # 5 - F - # 6 + FF - # 7 G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 2 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 6 }, # + FF - { new_path: file_name, new_line: 7 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, - { old_path: file_name, old_line: 2, change: true }, - { new_path: file_name, new_line: 2 }, - { old_path: file_name, new_path: file_name, old_line: 2, new_line: 3 }, - { new_path: file_name, new_line: 4 }, - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 5 }, - { old_path: file_name, old_line: 5 }, - { new_path: file_name, new_line: 6 }, - { new_path: file_name, new_line: 7 } - ] + describe '#cd_diffs' do + it 'returns the diffs in the new diff' do + diff_refs = subject.cd_diffs.diff_refs - expect_new_positions(old_position_attrs, new_position_attrs) + expect(diff_refs.base_sha).to eq(new_diff_refs.base_sha) + expect(diff_refs.start_sha).to eq(new_diff_refs.base_sha) + expect(diff_refs.head_sha).to eq(new_diff_refs.head_sha) end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index cceeae8afe6..a28b95e5bff 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1694,14 +1694,15 @@ describe Gitlab::Git::Repository, :seed_helper do let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } let(:left_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } let(:right_branch) { 'test-master' } + let(:first_parent_ref) { 'refs/heads/test-master' } let(:target_ref) { 'refs/merge-requests/999/merge' } before do - repository.create_branch(right_branch, branch_head) unless repository.branch_exists?(right_branch) + repository.create_branch(right_branch, branch_head) unless repository.ref_exists?(first_parent_ref) end def merge_to_ref - repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message') + repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message', first_parent_ref) end it 'generates a commit in the target_ref' do @@ -1716,7 +1717,7 @@ describe Gitlab::Git::Repository, :seed_helper do end it 'does not change the right branch HEAD' do - expect { merge_to_ref }.not_to change { repository.find_branch(right_branch).target } + expect { merge_to_ref }.not_to change { repository.commit(first_parent_ref).sha } end end diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index 18663a72fcd..f38b8d31237 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -79,13 +79,13 @@ describe Gitlab::GitalyClient::OperationService do end describe '#user_merge_to_ref' do - let(:branch) { 'my-branch' } + let(:first_parent_ref) { 'refs/heads/my-branch' } let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } let(:ref) { 'refs/merge-requests/x/merge' } let(:message) { 'validaciĆ³n' } let(:response) { Gitaly::UserMergeToRefResponse.new(commit_id: 'new-commit-id') } - subject { client.user_merge_to_ref(user, source_sha, branch, ref, message) } + subject { client.user_merge_to_ref(user, source_sha, nil, ref, message, first_parent_ref) } it 'sends a user_merge_to_ref message' do expect_any_instance_of(Gitaly::OperationService::Stub) diff --git a/spec/lib/gitlab/graphql/calls_gitaly/instrumentation_spec.rb b/spec/lib/gitlab/graphql/calls_gitaly/instrumentation_spec.rb new file mode 100644 index 00000000000..d93ce464a92 --- /dev/null +++ b/spec/lib/gitlab/graphql/calls_gitaly/instrumentation_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::Graphql::CallsGitaly::Instrumentation do + subject { described_class.new } + + describe '#calls_gitaly_check' do + let(:gitaly_field) { Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true) } + let(:no_gitaly_field) { Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: false) } + + context 'if there are no Gitaly calls' do + it 'does not raise an error if calls_gitaly is false' do + expect { subject.send(:calls_gitaly_check, no_gitaly_field, 0) }.not_to raise_error + end + end + + context 'if there is at least 1 Gitaly call' do + it 'raises an error if calls_gitaly: is false or not defined' do + expect { subject.send(:calls_gitaly_check, no_gitaly_field, 1) }.to raise_error(/specify a constant complexity or add `calls_gitaly: true`/) + end + end + end +end diff --git a/spec/lib/gitlab/namespaced_session_store_spec.rb b/spec/lib/gitlab/namespaced_session_store_spec.rb index c0af2ede32a..e177c44ad67 100644 --- a/spec/lib/gitlab/namespaced_session_store_spec.rb +++ b/spec/lib/gitlab/namespaced_session_store_spec.rb @@ -4,19 +4,33 @@ require 'spec_helper' describe Gitlab::NamespacedSessionStore do let(:key) { :some_key } - subject { described_class.new(key) } - it 'stores data under the specified key' do - Gitlab::Session.with_session({}) do - subject[:new_data] = 123 + context 'current session' do + subject { described_class.new(key) } - expect(Thread.current[:session_storage][key]).to eq(new_data: 123) + it 'stores data under the specified key' do + Gitlab::Session.with_session({}) do + subject[:new_data] = 123 + + expect(Thread.current[:session_storage][key]).to eq(new_data: 123) + end + end + + it 'retrieves data from the given key' do + Thread.current[:session_storage] = { key => { existing_data: 123 } } + + expect(subject[:existing_data]).to eq 123 end end - it 'retrieves data from the given key' do - Thread.current[:session_storage] = { key => { existing_data: 123 } } + context 'passed in session' do + let(:data) { { 'data' => 42 } } + let(:session) { { 'some_key' => data } } + + subject { described_class.new(key, session.with_indifferent_access) } - expect(subject[:existing_data]).to eq 123 + it 'retrieves data from the given key' do + expect(subject['data']).to eq 42 + end end end diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb index 7faa196623f..e2ab9ddd4a5 100644 --- a/spec/models/concerns/reactive_caching_spec.rb +++ b/spec/models/concerns/reactive_caching_spec.rb @@ -206,8 +206,9 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do expect(read_reactive_cache(instance)).to eq("preexisting") end - it 'enqueues a repeat worker' do - expect_reactive_cache_update_queued(instance) + it 'does not enqueue a repeat worker' do + expect(ReactiveCachingWorker) + .not_to receive(:perform_in) expect { go! }.to raise_error("foo") end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 8d0eb0f4a06..713fb647708 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -462,12 +462,6 @@ describe Deployment do it { is_expected.to be_nil } end - context 'project uses the kubernetes service for deployments' do - let!(:service) { create(:kubernetes_service, project: project) } - - it { is_expected.to be_nil } - end - context 'project has a deployment platform' do let!(:cluster) { create(:cluster, projects: [project]) } let!(:platform) { create(:cluster_platform_kubernetes, cluster: cluster) } diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb index 22df19d943f..a771d1bf27f 100644 --- a/spec/models/project_services/drone_ci_service_spec.rb +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -101,6 +101,15 @@ describe DroneCiService, :use_clean_rails_memory_store_caching do is_expected.to eq(:error) end + Gitlab::HTTP::HTTP_ERRORS.each do |http_error| + it "sets commit status to :error with a #{http_error.name} error" do + WebMock.stub_request(:get, commit_status_path) + .to_raise(http_error) + + is_expected.to eq(:error) + end + end + { "killed" => :canceled, "failure" => :failed, diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 5d7d6c34e67..d33bbb0470f 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -142,235 +142,6 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do end end - describe '#kubernetes_namespace_for' do - subject { service.kubernetes_namespace_for(project) } - - shared_examples 'a correctly formatted namespace' do - it 'returns a valid Kubernetes namespace name' do - expect(subject).to match(Gitlab::Regex.kubernetes_namespace_regex) - expect(subject).to eq(expected_namespace) - end - end - - it_behaves_like 'a correctly formatted namespace' do - let(:expected_namespace) { service.send(:default_namespace) } - end - - context 'when the project path contains forbidden characters' do - before do - project.path = '-a_Strange.Path--forSure' - end - - it_behaves_like 'a correctly formatted namespace' do - let(:expected_namespace) { "a-strange-path--forsure-#{project.id}" } - end - end - - context 'when namespace is specified' do - before do - service.namespace = 'my-namespace' - end - - it_behaves_like 'a correctly formatted namespace' do - let(:expected_namespace) { 'my-namespace' } - end - end - - context 'when service is not assigned to project' do - before do - service.project = nil - end - - it 'does not return namespace' do - is_expected.to be_nil - end - end - end - - describe '#test' do - let(:discovery_url) { 'https://kubernetes.example.com/api/v1' } - - before do - stub_kubeclient_discover(service.api_url) - end - - context 'with path prefix in api_url' do - let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' } - - it 'tests with the prefix' do - service.api_url = 'https://kubernetes.example.com/prefix' - stub_kubeclient_discover(service.api_url) - - expect(service.test[:success]).to be_truthy - expect(WebMock).to have_requested(:get, discovery_url).once - end - end - - context 'with custom CA certificate' do - it 'is added to the certificate store' do - service.ca_pem = "CA PEM DATA" - - cert = double("certificate") - expect(OpenSSL::X509::Certificate).to receive(:new).with(service.ca_pem).and_return(cert) - expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert) - - expect(service.test[:success]).to be_truthy - expect(WebMock).to have_requested(:get, discovery_url).once - end - end - - context 'success' do - it 'reads the discovery endpoint' do - expect(service.test[:success]).to be_truthy - expect(WebMock).to have_requested(:get, discovery_url).once - end - end - - context 'failure' do - it 'fails to read the discovery endpoint' do - WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(status: 404) - - expect(service.test[:success]).to be_falsy - expect(WebMock).to have_requested(:get, discovery_url).once - end - end - end - - describe '#predefined_variable' do - let(:kubeconfig) do - config_file = expand_fixture_path('config/kubeconfig.yml') - config = YAML.load(File.read(config_file)) - config.dig('users', 0, 'user')['token'] = 'token' - config.dig('contexts', 0, 'context')['namespace'] = namespace - config.dig('clusters', 0, 'cluster')['certificate-authority-data'] = - Base64.strict_encode64('CA PEM DATA') - - YAML.dump(config) - end - - before do - subject.api_url = 'https://kube.domain.com' - subject.token = 'token' - subject.ca_pem = 'CA PEM DATA' - subject.project = project - end - - shared_examples 'setting variables' do - it 'sets the variables' do - expect(subject.predefined_variables(project: project)).to include( - { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true }, - { key: 'KUBE_TOKEN', value: 'token', public: false, masked: true }, - { key: 'KUBE_NAMESPACE', value: namespace, public: true }, - { key: 'KUBECONFIG', value: kubeconfig, public: false, file: true }, - { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true }, - { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true } - ) - end - end - - context 'namespace is provided' do - let(:namespace) { 'my-project' } - - before do - subject.namespace = namespace - end - - it_behaves_like 'setting variables' - end - - context 'no namespace provided' do - let(:namespace) { subject.kubernetes_namespace_for(project) } - - it_behaves_like 'setting variables' - - it 'sets the KUBE_NAMESPACE' do - kube_namespace = subject.predefined_variables(project: project).find { |h| h[:key] == 'KUBE_NAMESPACE' } - - expect(kube_namespace).not_to be_nil - expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/) - end - end - end - - describe '#terminals' do - let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") } - - subject { service.terminals(environment) } - - context 'with invalid pods' do - it 'returns no terminals' do - stub_reactive_cache(service, pods: [{ "bad" => "pod" }]) - - is_expected.to be_empty - end - end - - context 'with valid pods' do - let(:pod) { kube_pod(environment_slug: environment.slug, namespace: service.kubernetes_namespace_for(project), project_slug: project.full_path_slug) } - let(:pod_with_no_terminal) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug, status: "Pending") } - let(:terminals) { kube_terminals(service, pod) } - - before do - stub_reactive_cache( - service, - pods: [pod, pod, pod_with_no_terminal, kube_pod(environment_slug: "should-be-filtered-out")] - ) - end - - it 'returns terminals' do - is_expected.to eq(terminals + terminals) - end - - it 'uses max session time from settings' do - stub_application_setting(terminal_max_session_time: 600) - - times = subject.map { |terminal| terminal[:max_session_time] } - expect(times).to eq [600, 600, 600, 600] - end - end - end - - describe '#calculate_reactive_cache' do - subject { service.calculate_reactive_cache } - - let(:namespace) { service.kubernetes_namespace_for(project) } - - context 'when service is inactive' do - before do - service.active = false - end - - it { is_expected.to be_nil } - end - - context 'when kubernetes responds with valid pods' do - before do - stub_kubeclient_pods(namespace) - stub_kubeclient_deployments(namespace) # Used by EE - end - - it { is_expected.to include(pods: [kube_pod]) } - end - - context 'when kubernetes responds with 500s' do - before do - stub_kubeclient_pods(namespace, status: 500) - stub_kubeclient_deployments(namespace, status: 500) # Used by EE - end - - it { expect { subject }.to raise_error(Kubeclient::HttpError) } - end - - context 'when kubernetes responds with 404s' do - before do - stub_kubeclient_pods(namespace, status: 404) - stub_kubeclient_deployments(namespace, status: 404) # Used by EE - end - - it { is_expected.to include(pods: []) } - end - end - describe "#deprecated?" do let(:kubernetes_service) { create(:kubernetes_service) } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 13da7bd7407..3d967aa4ab8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1420,12 +1420,13 @@ describe Repository do source_project: project) end - it 'writes merge of source and target to MR merge_ref_path' do + it 'writes merge of source SHA and first parent ref to MR merge_ref_path' do merge_commit_id = repository.merge_to_ref(user, merge_request.diff_head_sha, merge_request, merge_request.merge_ref_path, - 'Custom message') + 'Custom message', + merge_request.target_branch_ref) merge_commit = repository.commit(merge_commit_id) diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb index 656d6f8b50b..67371cb35b6 100644 --- a/spec/requests/api/graphql_spec.rb +++ b/spec/requests/api/graphql_spec.rb @@ -131,4 +131,35 @@ describe 'GraphQL' do end end end + + describe 'testing for Gitaly calls' do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + let(:query) do + graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id)) + end + + before do + project.add_developer(user) + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: user) + end + end + + context 'when Gitaly is called' do + before do + allow(Gitlab::GitalyClient).to receive(:get_request_count).and_return(1, 2) + end + + it "logs a warning that the 'calls_gitaly' field declaration is missing" do + expect(Gitlab::Sentry).to receive(:track_exception).once + + post_graphql(query, current_user: user) + end + end + end end diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 5548e3fd01a..f5ce3a3570e 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -584,6 +584,34 @@ describe API::Runners do end end + context 'when valid order_by is provided' do + context 'when sort order is not specified' do + it 'return jobs in descending order' do + get api("/runners/#{project_runner.id}/jobs?order_by=id", admin) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(2) + expect(json_response.first).to include('id' => job_5.id) + end + end + + context 'when sort order is specified as asc' do + it 'return jobs sorted in ascending order' do + get api("/runners/#{project_runner.id}/jobs?order_by=id&sort=asc", admin) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(2) + expect(json_response.first).to include('id' => job_4.id) + end + end + end + context 'when invalid status is provided' do it 'return 400' do get api("/runners/#{project_runner.id}/jobs?status=non-existing", admin) @@ -591,6 +619,22 @@ describe API::Runners do expect(response).to have_gitlab_http_status(400) end end + + context 'when invalid order_by is provided' do + it 'return 400' do + get api("/runners/#{project_runner.id}/jobs?order_by=non-existing", admin) + + expect(response).to have_gitlab_http_status(400) + end + end + + context 'when invalid sort is provided' do + it 'return 400' do + get api("/runners/#{project_runner.id}/jobs?sort=non-existing", admin) + + expect(response).to have_gitlab_http_status(400) + end + end end context "when runner doesn't exist" do diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb index 24cb63a0d61..a409f21a7e4 100644 --- a/spec/services/auto_merge/base_service_spec.rb +++ b/spec/services/auto_merge/base_service_spec.rb @@ -121,11 +121,7 @@ describe AutoMerge::BaseService do end end - describe '#cancel' do - subject { service.cancel(merge_request) } - - let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } - + shared_examples_for 'Canceled or Dropped' do it 'removes properies from the merge request' do subject @@ -173,6 +169,20 @@ describe AutoMerge::BaseService do it 'does not yield block' do expect { |b| service.execute(merge_request, &b) }.not_to yield_control end + end + end + + describe '#cancel' do + subject { service.cancel(merge_request) } + + let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } + + it_behaves_like 'Canceled or Dropped' + + context 'when failed to save' do + before do + allow(merge_request).to receive(:save) { false } + end it 'returns error status' do expect(subject[:status]).to eq(:error) @@ -180,4 +190,24 @@ describe AutoMerge::BaseService do end end end + + describe '#abort' do + subject { service.abort(merge_request, reason) } + + let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } + let(:reason) { 'an error'} + + it_behaves_like 'Canceled or Dropped' + + context 'when failed to save' do + before do + allow(merge_request).to receive(:save) { false } + end + + it 'returns error status' do + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to eq("Can't abort the automatic merge") + end + end + end end diff --git a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb index 5e84ef052ce..931b52470c4 100644 --- a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb @@ -177,6 +177,17 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do end end + describe "#abort" do + before do + service.abort(mr_merge_if_green_enabled, 'an error') + end + + it 'posts a system note' do + note = mr_merge_if_green_enabled.notes.last + expect(note.note).to include 'aborted the automatic merge' + end + end + describe 'pipeline integration' do context 'when there are multiple stages in the pipeline' do let(:ref) { mr_merge_if_green_enabled.source_branch } diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb index 93a22e60498..50dfc49a59c 100644 --- a/spec/services/auto_merge_service_spec.rb +++ b/spec/services/auto_merge_service_spec.rb @@ -161,4 +161,29 @@ describe AutoMergeService do end end end + + describe '#abort' do + subject { service.abort(merge_request, error) } + + let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } + let(:error) { 'an error' } + + it 'delegates to a relevant service instance' do + expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service| + expect(service).to receive(:abort).with(merge_request, error) + end + + subject + end + + context 'when auto merge is not enabled' do + let(:merge_request) { create(:merge_request) } + + it 'returns error' do + expect(subject[:message]).to eq("Can't abort the automatic merge") + expect(subject[:status]).to eq(:error) + expect(subject[:http_status]).to eq(406) + end + end + end end diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb index 61f99f82a76..e2f201677fa 100644 --- a/spec/services/merge_requests/merge_to_ref_service_spec.rb +++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb @@ -22,7 +22,6 @@ describe MergeRequests::MergeToRefService do shared_examples_for 'successfully merges to ref with merge method' do it 'writes commit to merge ref' do repository = project.repository - target_ref = merge_request.merge_ref_path expect(repository.ref_exists?(target_ref)).to be(false) @@ -33,7 +32,7 @@ describe MergeRequests::MergeToRefService do expect(result[:status]).to eq(:success) expect(result[:commit_id]).to be_present expect(result[:source_id]).to eq(merge_request.source_branch_sha) - expect(result[:target_id]).to eq(merge_request.target_branch_sha) + expect(result[:target_id]).to eq(repository.commit(first_parent_ref).sha) expect(repository.ref_exists?(target_ref)).to be(true) expect(ref_head.id).to eq(result[:commit_id]) end @@ -74,17 +73,22 @@ describe MergeRequests::MergeToRefService do describe '#execute' do let(:service) do - described_class.new(project, user, commit_message: 'Awesome message', - should_remove_source_branch: true) + described_class.new(project, user, **params) end + let(:params) { { commit_message: 'Awesome message', should_remove_source_branch: true } } + def process_merge_to_ref perform_enqueued_jobs do service.execute(merge_request) end end - it_behaves_like 'successfully merges to ref with merge method' + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/heads/master' } + let(:target_ref) { merge_request.merge_ref_path } + end + it_behaves_like 'successfully evaluates pre-condition checks' context 'commit history comparison with regular MergeService' do @@ -129,14 +133,22 @@ describe MergeRequests::MergeToRefService do context 'when semi-linear merge method' do let(:merge_method) { :rebase_merge } - it_behaves_like 'successfully merges to ref with merge method' + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/heads/master' } + let(:target_ref) { merge_request.merge_ref_path } + end + it_behaves_like 'successfully evaluates pre-condition checks' end context 'when fast-forward merge method' do let(:merge_method) { :ff } - it_behaves_like 'successfully merges to ref with merge method' + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/heads/master' } + let(:target_ref) { merge_request.merge_ref_path } + end + it_behaves_like 'successfully evaluates pre-condition checks' end @@ -178,5 +190,34 @@ describe MergeRequests::MergeToRefService do it { expect(todo).not_to be_done } end + + describe 'cascading merge refs' do + set(:project) { create(:project, :repository) } + let(:params) { { commit_message: 'Cascading merge', first_parent_ref: first_parent_ref, target_ref: target_ref } } + + context 'when first merge happens' do + let(:merge_request) do + create(:merge_request, source_project: project, source_branch: 'feature', + target_project: project, target_branch: 'master') + end + + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/heads/master' } + let(:target_ref) { 'refs/merge-requests/1/train' } + end + + context 'when second merge happens' do + let(:merge_request) do + create(:merge_request, source_project: project, source_branch: 'improve/awesome', + target_project: project, target_branch: 'master') + end + + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/merge-requests/1/train' } + let(:target_ref) { 'refs/merge-requests/2/train' } + end + end + end + end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 9f60e49290e..157cfc46e69 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -359,6 +359,22 @@ describe SystemNoteService do end end + describe '.abort_merge_when_pipeline_succeeds' do + let(:noteable) do + create(:merge_request, source_project: project, target_project: project) + end + + subject { described_class.abort_merge_when_pipeline_succeeds(noteable, project, author, 'merge request was closed') } + + it_behaves_like 'a system note' do + let(:action) { 'merge' } + end + + it "posts the 'merge when pipeline succeeds' system note" do + expect(subject.note).to eq "aborted the automatic merge because merge request was closed" + end + end + describe '.change_title' do let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') } @@ -1175,16 +1191,30 @@ describe SystemNoteService do end it 'links to the diff in the system note' do - expect(subject.note).to include('version 1') - diff_id = merge_request.merge_request_diff.id line_code = change_position.line_code(project.repository) - expect(subject.note).to include(diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: line_code)) + link = diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: line_code) + + expect(subject.note).to eq("changed this line in [version 1 of the diff](#{link})") + end + + context 'discussion is on an image' do + let(:discussion) { create(:image_diff_note_on_merge_request, project: project).to_discussion } + + it 'links to the diff in the system note' do + diff_id = merge_request.merge_request_diff.id + file_hash = change_position.file_hash + link = diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: file_hash) + + expect(subject.note).to eq("changed this file in [version 1 of the diff](#{link})") + end end end - context 'when the change_position is invalid for the discussion' do - let(:change_position) { project.commit(sample_commit.id) } + context 'when the change_position does not point to a valid version' do + before do + allow(merge_request).to receive(:version_params_for).and_return(nil) + end it 'creates a new note in the discussion' do # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded. diff --git a/spec/support/helpers/position_tracer_helpers.rb b/spec/support/helpers/position_tracer_helpers.rb new file mode 100644 index 00000000000..bbf6e06dd40 --- /dev/null +++ b/spec/support/helpers/position_tracer_helpers.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module PositionTracerHelpers + def diff_refs(base_commit, head_commit) + Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id) + end + + def position(attrs = {}) + attrs.reverse_merge!( + diff_refs: old_diff_refs + ) + Gitlab::Diff::Position.new(attrs) + end + + def expect_new_position(attrs, result = subject) + aggregate_failures("expect new position #{attrs.inspect}") do + if attrs.nil? + expect(result[:outdated]).to be_truthy + else + new_position = result[:position] + + expect(result[:outdated]).to be_falsey + expect(new_position).not_to be_nil + expect(new_position.diff_refs).to eq(new_diff_refs) + + attrs.each do |attr, value| + expect(new_position.send(attr)).to eq(value) + end + end + end + end + + def expect_change_position(attrs, result = subject) + aggregate_failures("expect change position #{attrs.inspect}") do + change_position = result[:position] + + expect(result[:outdated]).to be_truthy + + if attrs.nil? || attrs.empty? + expect(change_position).to be_nil + else + expect(change_position).not_to be_nil + expect(change_position.diff_refs).to eq(change_diff_refs) + + attrs.each do |attr, value| + expect(change_position.send(attr)).to eq(value) + end + end + end + end + + def create_branch(new_name, branch_name) + CreateBranchService.new(project, current_user).execute(new_name, branch_name) + end + + def create_file(branch_name, file_name, content) + Files::CreateService.new( + project, + current_user, + start_branch: branch_name, + branch_name: branch_name, + commit_message: "Create file", + file_path: file_name, + file_content: content + ).execute + project.commit(branch_name) + end + + def update_file(branch_name, file_name, content) + Files::UpdateService.new( + project, + current_user, + start_branch: branch_name, + branch_name: branch_name, + commit_message: "Update file", + file_path: file_name, + file_content: content + ).execute + project.commit(branch_name) + end + + def delete_file(branch_name, file_name) + Files::DeleteService.new( + project, + current_user, + start_branch: branch_name, + branch_name: branch_name, + commit_message: "Delete file", + file_path: file_name + ).execute + project.commit(branch_name) + end +end diff --git a/yarn.lock b/yarn.lock index 1e04c82df1c..b76eba830b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -705,10 +705,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.66.0.tgz#3c02da455421ea241f32e915671842435df027ff" integrity sha512-nxOoQPnofMs3BjRr3SVzQcclM0G6QFrLM8L4nnUCN+8Gxq2u8ukfSU5FCrkivXz+FP9Qo/FYilWV7CY8kDkt6A== -"@gitlab/ui@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.1.0.tgz#b8c8f266edc68f616e92c0ba3a18a692002393e4" - integrity sha512-IPgk5W7mSXcbni+zNuJeVU89Co72jSQAXTxU7AtmItt5XT6nI9US2ZAWNUl8XCiOOw81jzYv0PLp4bMiXdLkww== +"@gitlab/ui@^5.3.2": + version "5.3.2" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.3.2.tgz#8ee906cf0586834de0077e165f25764c0bf8a9e9" + integrity sha512-VgxlDXqG2q+u72Km+/Ljdvjh0DzvljvsztiXTxnOO+Eb+/I26JBWfdboqFr3E02JzT8W4s4rRinhRttLWfcM/A== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.2.1" |