diff options
author | Douwe Maan <douwe@gitlab.com> | 2018-02-01 20:22:04 +0000 |
---|---|---|
committer | Douwe Maan <douwe@gitlab.com> | 2018-02-01 20:22:04 +0000 |
commit | 26debb120e6375175b1ffd83343b9974d8c6d060 (patch) | |
tree | 35a18214d3d1e916ed6dc2b2266338526f2ca726 | |
parent | fd46d6ceb81eb9039b4e60c1d158848dd22ba411 (diff) | |
parent | cf644fc1ff9890189ad2945fc38c7e6b9943cae3 (diff) | |
download | gitlab-ce-zj-ref-contains.tar.gz |
Merge branch 'master' into 'zj-ref-contains'zj-ref-contains
# Conflicts:
# lib/gitlab/git/repository.rb
59 files changed, 839 insertions, 223 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index be18520b876..b4afa953175 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -738,8 +738,9 @@ cache gems: gitlab_git_test: <<: *dedicated-runner <<: *except-docs-and-qa - <<: *pull-cache variables: SETUP_DB: "false" + before_script: [] + cache: {} script: - spec/support/prepare-gitlab-git-test-for-commit --check-for-changes diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 62df9f538d8..4a7076db09a 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.76.0 +0.77.0 diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index ca3d271663b..c7bccd483ac 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -1,5 +1,6 @@ /* eslint-disable no-new */ -import Flash from './flash'; +import flash from './flash'; +import axios from './lib/utils/axios_utils'; /** * In each pipelines table we have a mini pipeline graph for each pipeline. @@ -78,27 +79,22 @@ export default class MiniPipelineGraph { const button = e.relatedTarget; const endpoint = button.dataset.stageEndpoint; - return $.ajax({ - dataType: 'json', - type: 'GET', - url: endpoint, - beforeSend: () => { - this.renderBuildsList(button, ''); - this.toggleLoading(button); - }, - success: (data) => { + this.renderBuildsList(button, ''); + this.toggleLoading(button); + + axios.get(endpoint) + .then(({ data }) => { this.toggleLoading(button); this.renderBuildsList(button, data.html); this.stopDropdownClickPropagation(); - }, - error: () => { + }) + .catch(() => { this.toggleLoading(button); if ($(button).parent().hasClass('open')) { $(button).dropdown('toggle'); } - new Flash('An error occurred while fetching the builds.', 'alert'); - }, - }); + flash('An error occurred while fetching the builds.', 'alert'); + }); } /** diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index 5aad3908eb6..d3edcb724f1 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -1,5 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */ +import { __ } from '../locale'; +import axios from '../lib/utils/axios_utils'; +import flash from '../flash'; import Raphael from './raphael'; export default (function() { @@ -26,16 +29,13 @@ export default (function() { } BranchGraph.prototype.load = function() { - return $.ajax({ - url: this.options.url, - method: "get", - dataType: "json", - success: $.proxy(function(data) { + axios.get(this.options.url) + .then(({ data }) => { $(".loading", this.element).hide(); this.prepareData(data.days, data.commits); - return this.buildGraph(); - }, this) - }); + this.buildGraph(); + }) + .catch(() => __('Error fetching network graph.')); }; BranchGraph.prototype.prepareData = function(days, commits) { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index a2b8e6f6495..bcb342f407f 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -16,6 +16,7 @@ import Autosize from 'autosize'; import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.atwho'; import AjaxCache from '~/lib/utils/ajax_cache'; +import axios from './lib/utils/axios_utils'; import { getLocationHash } from './lib/utils/url_utility'; import Flash from './flash'; import CommentTypeToggle from './comment_type_toggle'; @@ -252,26 +253,20 @@ export default class Notes { return; } this.refreshing = true; - return $.ajax({ - url: this.notes_url, - headers: { 'X-Last-Fetched-At': this.last_fetched_at }, - dataType: 'json', - success: (function(_this) { - return function(data) { - var notes; - notes = data.notes; - _this.last_fetched_at = data.last_fetched_at; - _this.setPollingInterval(data.notes.length); - return $.each(notes, function(i, note) { - _this.renderNote(note); - }); - }; - })(this) - }).always((function(_this) { - return function() { - return _this.refreshing = false; - }; - })(this)); + axios.get(this.notes_url, { + headers: { + 'X-Last-Fetched-At': this.last_fetched_at, + }, + }).then(({ data }) => { + const notes = data.notes; + this.last_fetched_at = data.last_fetched_at; + this.setPollingInterval(data.notes.length); + $.each(notes, (i, note) => this.renderNote(note)); + + this.refreshing = false; + }).catch(() => { + this.refreshing = false; + }); } /** diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js index 4534360d577..4e0afe13590 100644 --- a/app/assets/javascripts/notifications_form.js +++ b/app/assets/javascripts/notifications_form.js @@ -1,3 +1,7 @@ +import { __ } from './locale'; +import axios from './lib/utils/axios_utils'; +import flash from './flash'; + export default class NotificationsForm { constructor() { this.toggleCheckbox = this.toggleCheckbox.bind(this); @@ -27,24 +31,20 @@ export default class NotificationsForm { saveEvent($checkbox, $parent) { const form = $parent.parents('form:first'); - return $.ajax({ - url: form.attr('action'), - method: form.attr('method'), - dataType: 'json', - data: form.serialize(), - beforeSend: () => { - this.showCheckboxLoadingSpinner($parent); - }, - }).done((data) => { - $checkbox.enable(); - if (data.saved) { - $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); - setTimeout(() => { - $parent.removeClass('is-loading') - .find('.custom-notification-event-loading') - .toggleClass('fa-spin fa-spinner fa-check is-done'); - }, 2000); - } - }); + this.showCheckboxLoadingSpinner($parent); + + axios[form.attr('method')](form.attr('action'), form.serialize()) + .then(({ data }) => { + $checkbox.enable(); + if (data.saved) { + $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); + setTimeout(() => { + $parent.removeClass('is-loading') + .find('.custom-notification-event-loading') + .toggleClass('fa-spin fa-spinner fa-check is-done'); + }, 2000); + } + }) + .catch(() => flash(__('There was an error saving your notification settings.'))); } } diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index 6552a88b606..fd3105b1960 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -1,4 +1,5 @@ import { getParameterByName } from '~/lib/utils/common_utils'; +import axios from './lib/utils/axios_utils'; import { removeParams } from './lib/utils/url_utility'; const ENDLESS_SCROLL_BOTTOM_PX = 400; @@ -22,24 +23,22 @@ export default { getOld() { this.loading.show(); - $.ajax({ - type: 'GET', - url: this.url, - data: `limit=${this.limit}&offset=${this.offset}`, - dataType: 'json', - error: () => this.loading.hide(), - success: (data) => { - this.append(data.count, this.prepareData(data.html)); - this.callback(); - - // keep loading until we've filled the viewport height - if (!this.disable && !this.isScrollable()) { - this.getOld(); - } else { - this.loading.hide(); - } + axios.get(this.url, { + params: { + limit: this.limit, + offset: this.offset, }, - }); + }).then(({ data }) => { + this.append(data.count, this.prepareData(data.html)); + this.callback(); + + // keep loading until we've filled the viewport height + if (!this.disable && !this.isScrollable()) { + this.getOld(); + } else { + this.loading.hide(); + } + }).catch(() => this.loading.hide()); }, append(count, html) { diff --git a/app/assets/javascripts/pages/admin/cohorts/usage_ping.js b/app/assets/javascripts/pages/admin/cohorts/usage_ping.js index 2389056bd02..914a9661c27 100644 --- a/app/assets/javascripts/pages/admin/cohorts/usage_ping.js +++ b/app/assets/javascripts/pages/admin/cohorts/usage_ping.js @@ -1,12 +1,13 @@ +import axios from '../../../lib/utils/axios_utils'; +import { __ } from '../../../locale'; +import flash from '../../../flash'; + export default function UsagePing() { - const usageDataUrl = $('.usage-data').data('endpoint'); + const el = document.querySelector('.usage-data'); - $.ajax({ - type: 'GET', - url: usageDataUrl, - dataType: 'html', - success(html) { - $('.usage-data').html(html); - }, - }); + axios.get(el.dataset.endpoint, { + responseType: 'text', + }).then(({ data }) => { + el.innerHTML = data; + }).catch(() => flash(__('Error fetching usage ping data.'))); } diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index e976a3d2f1d..b3f6a72fdcb 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -2,6 +2,9 @@ import { visitUrl } from '~/lib/utils/url_utility'; import UsersSelect from '~/users_select'; import { isMetaClick } from '~/lib/utils/common_utils'; +import { __ } from '../../../../locale'; +import flash from '../../../../flash'; +import axios from '../../../../lib/utils/axios_utils'; export default class Todos { constructor() { @@ -59,18 +62,12 @@ export default class Todos { const target = e.target; target.setAttribute('disabled', true); target.classList.add('disabled'); - $.ajax({ - type: 'POST', - url: target.dataset.href, - dataType: 'json', - data: { - '_method': target.dataset.method, - }, - success: (data) => { + + axios[target.dataset.method](target.dataset.href) + .then(({ data }) => { this.updateRowState(target); - return this.updateBadges(data); - }, - }); + this.updateBadges(data); + }).catch(() => flash(__('Error updating todo status.'))); } updateRowState(target) { @@ -98,19 +95,15 @@ export default class Todos { e.preventDefault(); const target = e.currentTarget; - const requestData = { '_method': target.dataset.method, ids: this.todo_ids }; target.setAttribute('disabled', true); target.classList.add('disabled'); - $.ajax({ - type: 'POST', - url: target.dataset.href, - dataType: 'json', - data: requestData, - success: (data) => { - this.updateAllState(target, data); - return this.updateBadges(data); - }, - }); + + axios[target.dataset.method](target.dataset.href, { + ids: this.todo_ids, + }).then(({ data }) => { + this.updateAllState(target, data); + this.updateBadges(data); + }).catch(() => flash(__('Error updating status for all todos.'))); } updateAllState(target, data) { diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js index 28a0160f47d..c4b3356e478 100644 --- a/app/assets/javascripts/pages/projects/tree/show/index.js +++ b/app/assets/javascripts/pages/projects/tree/show/index.js @@ -1,3 +1,5 @@ +import Vue from 'vue'; +import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; import TreeView from '../../../../tree'; import ShortcutsNavigation from '../../../../shortcuts_navigation'; import BlobViewer from '../../../../blob/viewer'; @@ -11,5 +13,25 @@ export default () => { new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new $('#tree-slider').waitForImages(() => ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath)); + + const commitPipelineStatusEl = document.getElementById('commit-pipeline-status'); + const statusLink = document.querySelector('.commit-actions .ci-status-link'); + if (statusLink != null) { + statusLink.remove(); + // eslint-disable-next-line no-new + new Vue({ + el: commitPipelineStatusEl, + components: { + commitPipelineStatus, + }, + render(createElement) { + return createElement('commit-pipeline-status', { + props: { + endpoint: commitPipelineStatusEl.dataset.endpoint, + }, + }); + }, + }); + } }; diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue new file mode 100644 index 00000000000..63f20a0041d --- /dev/null +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -0,0 +1,120 @@ +<script> + import Visibility from 'visibilityjs'; + import ciIcon from '~/vue_shared/components/ci_icon.vue'; + import loadingIcon from '~/vue_shared/components/loading_icon.vue'; + import Poll from '~/lib/utils/poll'; + import Flash from '~/flash'; + import { s__, sprintf } from '~/locale'; + import tooltip from '~/vue_shared/directives/tooltip'; + import CommitPipelineService from '../services/commit_pipeline_service'; + + export default { + directives: { + tooltip, + }, + components: { + ciIcon, + loadingIcon, + }, + props: { + endpoint: { + type: String, + required: true, + }, + /* This prop can be used to replace some of the `render_commit_status` + used across GitLab, this way we could use this vue component and add a + realtime status where it makes sense + realtime: { + type: Boolean, + required: false, + default: true, + }, */ + }, + data() { + return { + ciStatus: {}, + isLoading: true, + }; + }, + computed: { + statusTitle() { + return sprintf(s__('Commits|Commit: %{commitText}'), { commitText: this.ciStatus.text }); + }, + }, + mounted() { + this.service = new CommitPipelineService(this.endpoint); + this.initPolling(); + }, + methods: { + successCallback(res) { + const pipelines = res.data.pipelines; + if (pipelines.length > 0) { + // The pipeline entity always keeps the latest pipeline info on the `details.status` + this.ciStatus = pipelines[0].details.status; + } + this.isLoading = false; + }, + errorCallback() { + this.ciStatus = { + text: 'not found', + icon: 'status_notfound', + group: 'notfound', + }; + this.isLoading = false; + Flash(s__('Something went wrong on our end')); + }, + initPolling() { + this.poll = new Poll({ + resource: this.service, + method: 'fetchData', + successCallback: response => this.successCallback(response), + errorCallback: this.errorCallback, + }); + + if (!Visibility.hidden()) { + this.isLoading = true; + this.poll.makeRequest(); + } else { + this.fetchPipelineCommitData(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); + }, + fetchPipelineCommitData() { + this.service.fetchData() + .then(this.successCallback) + .catch(this.errorCallback); + }, + }, + destroy() { + this.poll.stop(); + }, + }; +</script> +<template> + <div> + <loading-icon + label="Loading pipeline status" + size="3" + v-if="isLoading" + /> + <a + v-else + :href="ciStatus.details_path" + > + <ci-icon + v-tooltip + :title="statusTitle" + :aria-label="statusTitle" + data-container="body" + :status="ciStatus" + /> + </a> + </div> +</template> diff --git a/app/assets/javascripts/projects/tree/services/commit_pipeline_service.js b/app/assets/javascripts/projects/tree/services/commit_pipeline_service.js new file mode 100644 index 00000000000..4b4189bc2de --- /dev/null +++ b/app/assets/javascripts/projects/tree/services/commit_pipeline_service.js @@ -0,0 +1,11 @@ +import axios from '~/lib/utils/axios_utils'; + +export default class CommitPipelineService { + constructor(endpoint) { + this.endpoint = endpoint; + } + + fetchData() { + return axios.get(this.endpoint); + } +} diff --git a/app/assets/javascripts/vue_shared/components/confirmation_input.vue b/app/assets/javascripts/vue_shared/components/confirmation_input.vue new file mode 100644 index 00000000000..1aa03ea6317 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/confirmation_input.vue @@ -0,0 +1,62 @@ +<script> + import _ from 'underscore'; + import { __, sprintf } from '~/locale'; + + export default { + props: { + inputId: { + type: String, + required: true, + }, + confirmationKey: { + type: String, + required: true, + }, + confirmationValue: { + type: String, + required: true, + }, + shouldEscapeConfirmationValue: { + type: Boolean, + required: false, + default: true, + }, + }, + computed: { + inputLabel() { + let value = this.confirmationValue; + if (this.shouldEscapeConfirmationValue) { + value = _.escape(value); + } + + return sprintf( + __('Type %{value} to confirm:'), + { value: `<code>${value}</code>` }, + false, + ); + }, + }, + methods: { + hasCorrectValue() { + return this.$refs.enteredValue.value === this.confirmationValue; + }, + }, + }; +</script> + +<template> + <div> + <label + v-html="inputLabel" + :for="inputId" + > + </label> + <input + :id="inputId" + :name="confirmationKey" + type="text" + ref="enteredValue" + class="form-control" + /> + </div> +</template> diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index aeaa33bd3bd..17801ed5910 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -195,6 +195,18 @@ .commit-actions { @media (min-width: $screen-sm-min) { font-size: 0; + + div { + display: inline; + } + + .fa-spinner { + font-size: 12px; + } + + span { + font-size: 6px; + } } .ci-status-link { @@ -219,6 +231,11 @@ font-size: 14px; font-weight: $gl-font-weight-bold; } + + .ci-status-icon { + position: relative; + top: 1px; + } } .commit, diff --git a/app/policies/ci/pipeline_schedule_policy.rb b/app/policies/ci/pipeline_schedule_policy.rb index abcf536b2f7..dc7a4aed577 100644 --- a/app/policies/ci/pipeline_schedule_policy.rb +++ b/app/policies/ci/pipeline_schedule_policy.rb @@ -10,6 +10,10 @@ module Ci can?(:developer_access) && pipeline_schedule.owned_by?(@user) end + condition(:non_owner_of_schedule) do + !pipeline_schedule.owned_by?(@user) + end + rule { can?(:developer_access) }.policy do enable :play_pipeline_schedule end @@ -19,6 +23,10 @@ module Ci enable :admin_pipeline_schedule end + rule { can?(:master_access) & non_owner_of_schedule }.policy do + enable :take_ownership_pipeline_schedule + end + rule { protected_ref }.prevent :play_pipeline_schedule end end diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index d66066a6d0b..90272ad9554 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -51,6 +51,7 @@ - if commit.status(ref) = render_commit_status(commit, ref: ref) + #commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id) } } = link_to commit.short_id, link, class: "commit-sha btn btn-transparent btn-link" = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard")) = link_to_browse_code(project, commit) diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml index 800e234275c..a8692b83b07 100644 --- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml +++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml @@ -29,9 +29,10 @@ - if can?(current_user, :play_pipeline_schedule, pipeline_schedule) = link_to play_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('Play'), class: 'btn' do = icon('play') - - if can?(current_user, :update_pipeline_schedule, pipeline_schedule) + - if can?(current_user, :take_ownership_pipeline_schedule, pipeline_schedule) = link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn' do = s_('PipelineSchedules|Take ownership') + - if can?(current_user, :update_pipeline_schedule, pipeline_schedule) = link_to edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), class: 'btn' do = icon('pencil') - if can?(current_user, :admin_pipeline_schedule, pipeline_schedule) diff --git a/changelogs/unreleased/35285-user-interface-bugs-for-schedule-pipelines.yml b/changelogs/unreleased/35285-user-interface-bugs-for-schedule-pipelines.yml new file mode 100644 index 00000000000..f3a04469884 --- /dev/null +++ b/changelogs/unreleased/35285-user-interface-bugs-for-schedule-pipelines.yml @@ -0,0 +1,5 @@ +--- +title: Hide pipeline schedule take ownership for current owner +merge_request: 12986 +author: +type: fixed diff --git a/changelogs/unreleased/35779-realtime-update-of-pipeline-status-in-files-view.yml b/changelogs/unreleased/35779-realtime-update-of-pipeline-status-in-files-view.yml new file mode 100644 index 00000000000..82df00fe631 --- /dev/null +++ b/changelogs/unreleased/35779-realtime-update-of-pipeline-status-in-files-view.yml @@ -0,0 +1,5 @@ +--- +title: Add realtime ci status for the repository -> files view +merge_request: 16523 +author: +type: added diff --git a/changelogs/unreleased/add-confirmation-input-for-modals.yml b/changelogs/unreleased/add-confirmation-input-for-modals.yml new file mode 100644 index 00000000000..ff1027bc55a --- /dev/null +++ b/changelogs/unreleased/add-confirmation-input-for-modals.yml @@ -0,0 +1,5 @@ +--- +title: Add confirmation-input component +merge_request: 16816 +author: +type: other diff --git a/doc/README.md b/doc/README.md index 330670587f5..c8b6b4f32b8 100644 --- a/doc/README.md +++ b/doc/README.md @@ -8,23 +8,13 @@ comments: false Welcome to [GitLab](https://about.gitlab.com/), a Git-based fully featured platform for software development! -GitLab offers the most scalable Git-based fully integrated platform for software development, with flexible products and subscription plans: +GitLab offers the most scalable Git-based fully integrated platform for software development, with flexible products and subscription plans. -- **GitLab Community Edition (CE)** is an [open source product](https://gitlab.com/gitlab-org/gitlab-ce/), -self-hosted, free to use. Every feature available in GitLab CE is also available on GitLab Enterprise Edition (Starter and Premium) and GitLab.com. -- **GitLab Enterprise Edition (EE)** is an [open-core product](https://gitlab.com/gitlab-org/gitlab-ee/), -self-hosted, fully featured solution of GitLab, available under distinct [subscriptions](https://about.gitlab.com/products/): **GitLab Enterprise Edition Starter (EES)**, **GitLab Enterprise Edition Premium (EEP)**, and **GitLab Enterprise Edition Ultimate (EEU)**. -- **GitLab.com**: SaaS GitLab solution, with [free and paid subscriptions](https://about.gitlab.com/gitlab-com/). GitLab.com is hosted by GitLab, Inc., and administrated by GitLab (users don't have access to admin settings). +With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/products/): Libre, Starter, Premium, and Ultimate. -> **GitLab EE** contains all features available in **GitLab CE**, -plus premium features available in each version: **Enterprise Edition Starter** -(**EES**), **Enterprise Edition Premium** (**EEP**), and **Enterprise Edition Ultimate** -(**EEU**). Everything available in **EES** is also available in **EEP**. Every feature -available in **EEP** is also available in **EEU**. +GitLab.com is our SaaS offering. It's hosted, managed, and administered by GitLab, with [free and paid plans](https://about.gitlab.com/gitlab-com/) for individuals and teams: Free, Bronze, Silver, and Gold. ----- - -Shortcuts to GitLab's most visited docs: +## Shortcuts to GitLab's most visited docs | [GitLab CI/CD](ci/README.md) | Other | | :----- | :----- | @@ -134,14 +124,8 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i ## Administrator documentation -[Administration documentation](administration/index.md) applies to admin users of GitLab -self-hosted instances: - -- GitLab Community Edition -- GitLab [Enterprise Editions](https://about.gitlab.com/gitlab-ee/) - - Enterprise Edition Starter (EES) - - Enterprise Edition Premium (EEP) - - Enterprise Edition Ultimate (EEU) +[Administration documentation](administration/index.md) applies to admin users of [GitLab +self-hosted instances](#self-hosted-gitlab): Libre, Starter, Premium, Ultimate. Learn how to install, configure, update, upgrade, integrate, and maintain your own instance. Regular users don't have access to GitLab administration tools and settings. diff --git a/doc/development/automatic_ce_ee_merge.md b/doc/development/automatic_ce_ee_merge.md index 5a784b6de06..cf6314f9521 100644 --- a/doc/development/automatic_ce_ee_merge.md +++ b/doc/development/automatic_ce_ee_merge.md @@ -5,17 +5,32 @@ Enterprise Edition (look for the [`CE Upstream` merge requests]). This merge is done automatically in a [scheduled pipeline](https://gitlab.com/gitlab-org/release-tools/-/jobs/43201679). -If a merge is already in progress, the job [doesn't create a new one](https://gitlab.com/gitlab-org/release-tools/-/jobs/43157687). -**If you are pinged in a `CE Upstream` merge request to resolve a conflict, -please resolve the conflict as soon as possible or ask someone else to do it!** - ->**Note:** -It's ok to resolve more conflicts than the one that you are asked to resolve. In -that case, it's a good habit to ask for a double-check on your resolution by -someone who is familiar with the code you touched. +## What to do if you are pinged in a `CE Upstream` merge request to resolve a conflict? + +1. Please resolve the conflict as soon as possible or ask someone else to do it + - It's ok to resolve more conflicts than the one that you are asked to resolve. + In that case, it's a good habit to ask for a double-check on your resolution + by someone who is familiar with the code you touched. +1. Once you have resolved your conflicts, push to the branch (no force-push) +1. Assign the merge request to the next person that has to resolve a conflict +1. If all conflicts are resolved after your resolution is pushed, keep the merge + request assigned to you: **you are now responsible for the merge request to be + green** +1. If you need any help, you can ping the current [release managers], or ask in + the `#ce-to-ee` Slack channel + +A few notes about the automatic CE->EE merge job: + +- If a merge is already in progress, the job + [doesn't create a new one](https://gitlab.com/gitlab-org/release-tools/-/jobs/43157687). +- If there is nothing to merge (i.e. EE is up-to-date with CE), the job doesn't + create a new one +- The job posts messages to the `#ce-to-ee` Slack channel to inform what's the + current CE->EE merge status (e.g. "A new MR has been created", "A MR is still pending") [`CE Upstream` merge requests]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?label_name%5B%5D=CE+upstream +[release managers]: https://about.gitlab.com/release-managers/ ## Always merge EE merge requests before their CE counterparts diff --git a/doc/user/group/subgroups/img/create_subgroup_button.png b/doc/user/group/subgroups/img/create_subgroup_button.png Binary files differindex 000b54c2855..d1355d4b2c3 100644 --- a/doc/user/group/subgroups/img/create_subgroup_button.png +++ b/doc/user/group/subgroups/img/create_subgroup_button.png diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 161a3af9903..2a982344e5f 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -90,7 +90,8 @@ structure. To create a subgroup: -1. In the group's dashboard go to the **Subgroups** page and click **New subgroup**. +1. In the group's dashboard expand the **New project** split button, select + **New subgroup** and click the **New subgroup** button. ![Subgroups page](img/create_subgroup_button.png) diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index c77db0f685f..d4e893b881c 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -6,6 +6,7 @@ module Gitlab CommandError = Class.new(StandardError) CommitError = Class.new(StandardError) + OSError = Class.new(StandardError) class << self include Gitlab::EncodingHelper diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 95644dacc4e..4cf47f3f740 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -1306,7 +1306,15 @@ module Gitlab # rubocop:enable Metrics/ParameterLists def write_config(full_path:) - rugged.config['gitlab.fullpath'] = full_path if full_path.present? + return unless full_path.present? + + gitaly_migrate(:write_config) do |is_enabled| + if is_enabled + gitaly_repository_client.write_config(full_path: full_path) + else + rugged_write_config(full_path: full_path) + end + end end def gitaly_repository @@ -1464,6 +1472,10 @@ module Gitlab names end + def rugged_write_config(full_path:) + rugged.config['gitlab.fullpath'] = full_path + end + def shell_write_ref(ref_path, ref, old_ref) raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ') raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00") diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index 7adf32af209..60706b4f0d8 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -219,6 +219,19 @@ module Gitlab true end + + def write_config(full_path:) + request = Gitaly::WriteConfigRequest.new(repository: @gitaly_repo, full_path: full_path) + response = GitalyClient.call( + @storage, + :repository_service, + :write_config, + request, + timeout: GitalyClient.fast_timeout + ) + + raise Gitlab::Git::OSError.new(response.error) unless response.error.empty? + end end end end diff --git a/scripts/lint-rugged b/scripts/lint-rugged index 03f780f880b..cabd083e9f9 100755 --- a/scripts/lint-rugged +++ b/scripts/lint-rugged @@ -1,7 +1,7 @@ #!/usr/bin/env ruby ALLOWED = [ - # Can be deleted (?) once rugged is no longer used in production. Doesn't make Rugged calls. + # Can be fixed once Rugged is no longer used in production. Doesn't make Rugged calls. 'config/initializers/8_metrics.rb', # Can be deleted once wiki's are fully (mandatory) migrated @@ -13,9 +13,6 @@ ALLOWED = [ # Needs to be migrated, https://gitlab.com/gitlab-org/gitaly/issues/954 'lib/tasks/gitlab/cleanup.rake', - # https://gitlab.com/gitlab-org/gitaly/issues/961 - 'app/models/repository.rb', - # The only place where Rugged code is still allowed in production 'lib/gitlab/git/' ].freeze diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb index 84a8bc56640..d5d819d862a 100644 --- a/spec/factories/commits.rb +++ b/spec/factories/commits.rb @@ -23,7 +23,7 @@ FactoryBot.define do end after(:build) do |commit, evaluator| - allow(commit).to receive(:author).and_return(evaluator.author || build(:author)) + allow(commit).to receive(:author).and_return(evaluator.author || build_stubbed(:author)) end trait :without_author do diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb index 9d7d5e56611..cac56695319 100644 --- a/spec/factories/deployments.rb +++ b/spec/factories/deployments.rb @@ -3,13 +3,14 @@ FactoryBot.define do sha '97de212e80737a608d939f648d959671fb0a0142' ref 'master' tag false - user + user nil project nil deployable factory: :ci_build environment factory: :environment after(:build) do |deployment, evaluator| deployment.project ||= deployment.environment.project + deployment.user ||= deployment.project.creator unless deployment.project.repository_exists? allow(deployment.project.repository).to receive(:create_ref) diff --git a/spec/factories/events.rb b/spec/factories/events.rb index ed275243ac9..5798b81ecad 100644 --- a/spec/factories/events.rb +++ b/spec/factories/events.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :event do project - author factory: :user + author(factory: :user) { project.creator } action Event::JOINED trait(:created) { action Event::CREATED } diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb index 71dc169c6a2..998080a3dd5 100644 --- a/spec/factories/issues.rb +++ b/spec/factories/issues.rb @@ -1,8 +1,8 @@ FactoryBot.define do factory :issue do title { generate(:title) } - author project + author { project.creator } trait :confidential do confidential true diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 40558c88d15..d26cb0c3417 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -1,9 +1,9 @@ FactoryBot.define do factory :merge_request do title { generate(:title) } - author association :source_project, :repository, factory: :project target_project { source_project } + author { source_project.creator } # $ git log --pretty=oneline feature..master # 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 707ecbd6be5..2defb4935ad 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -6,7 +6,7 @@ FactoryBot.define do factory :note do project note { generate(:title) } - author + author { project&.creator || create(:user) } on_issue factory :note_on_commit, traits: [:on_commit] diff --git a/spec/factories/project_wikis.rb b/spec/factories/project_wikis.rb index 89d8248f9f4..db2eb4fc863 100644 --- a/spec/factories/project_wikis.rb +++ b/spec/factories/project_wikis.rb @@ -3,7 +3,7 @@ FactoryBot.define do skip_create project - user factory: :user + user { project.creator } initialize_with { new(project, user) } end end diff --git a/spec/factories/sent_notifications.rb b/spec/factories/sent_notifications.rb index 80872067233..b0174dd06b7 100644 --- a/spec/factories/sent_notifications.rb +++ b/spec/factories/sent_notifications.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :sent_notification do project - recipient factory: :user + recipient { project.creator } noteable { create(:issue, project: project) } reply_key { SentNotification.reply_key } end diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb index 2ab9a56d255..dc12b562108 100644 --- a/spec/factories/snippets.rb +++ b/spec/factories/snippets.rb @@ -21,6 +21,7 @@ FactoryBot.define do factory :project_snippet, parent: :snippet, class: :ProjectSnippet do project + author { project.creator } end factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do diff --git a/spec/factories/subscriptions.rb b/spec/factories/subscriptions.rb index a4bc4e87b0a..8f7ab74ec70 100644 --- a/spec/factories/subscriptions.rb +++ b/spec/factories/subscriptions.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :subscription do - user project + user { project.creator } subscribable factory: :issue end end diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb index af34b0681e2..b45f06b9a0a 100644 --- a/spec/factories/timelogs.rb +++ b/spec/factories/timelogs.rb @@ -3,7 +3,7 @@ FactoryBot.define do factory :timelog do time_spent 3600 - user issue + user { issue.project.creator } end end diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index 6a6de665dd1..94f8caedfa6 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -1,8 +1,8 @@ FactoryBot.define do factory :todo do project - author - user + author { project.creator } + user { project.creator } target factory: :issue action { Todo::ASSIGNED } diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index 991d360ccaf..744041ac425 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -44,36 +44,38 @@ feature 'Dashboard Merge Requests' do context 'merge requests exist' do let!(:assigned_merge_request) do - create(:merge_request, assignee: current_user, target_project: project, source_project: project) + create(:merge_request, + assignee: current_user, + source_project: project, + author: create(:user)) end let!(:assigned_merge_request_from_fork) do create(:merge_request, source_branch: 'markdown', assignee: current_user, - target_project: public_project, source_project: forked_project - ) + target_project: public_project, source_project: forked_project, + author: create(:user)) end let!(:authored_merge_request) do create(:merge_request, - source_branch: 'markdown', author: current_user, - target_project: project, source_project: project - ) + source_branch: 'markdown', + source_project: project, + author: current_user) end let!(:authored_merge_request_from_fork) do create(:merge_request, source_branch: 'feature_conflict', author: current_user, - target_project: public_project, source_project: forked_project - ) + target_project: public_project, source_project: forked_project) end let!(:other_merge_request) do create(:merge_request, source_branch: 'fix', - target_project: project, source_project: project - ) + source_project: project, + author: create(:user)) end before do diff --git a/spec/features/merge_request/user_awards_emoji_spec.rb b/spec/features/merge_request/user_awards_emoji_spec.rb index 15a0878fb16..2f24cfbd9e3 100644 --- a/spec/features/merge_request/user_awards_emoji_spec.rb +++ b/spec/features/merge_request/user_awards_emoji_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' describe 'Merge request > User awards emoji', :js do let(:project) { create(:project, :public, :repository) } let(:user) { project.creator } - let(:merge_request) { create(:merge_request, source_project: project) } + let(:merge_request) { create(:merge_request, source_project: project, author: create(:user)) } describe 'logged in' do before do diff --git a/spec/fixtures/api/schemas/public_api/v4/user/basic.json b/spec/fixtures/api/schemas/public_api/v4/user/basic.json index bf330d8278c..2d815be32a6 100644 --- a/spec/fixtures/api/schemas/public_api/v4/user/basic.json +++ b/spec/fixtures/api/schemas/public_api/v4/user/basic.json @@ -2,12 +2,16 @@ "type": ["object", "null"], "required": [ "id", + "name", + "username", "state", "avatar_url", "web_url" ], "properties": { "id": { "type": "integer" }, + "name": { "type": "string" }, + "username": { "type": "string" }, "state": { "type": "string" }, "avatar_url": { "type": "string" }, "web_url": { "type": "string" } diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js new file mode 100644 index 00000000000..90f290e845e --- /dev/null +++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js @@ -0,0 +1,104 @@ +import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Commit pipeline status component', () => { + let vm; + let Component; + let mock; + const mockCiStatus = { + details_path: '/root/hello-world/pipelines/1', + favicon: 'canceled.ico', + group: 'canceled', + has_details: true, + icon: 'status_canceled', + label: 'canceled', + text: 'canceled', + }; + + beforeEach(() => { + Component = Vue.extend(commitPipelineStatus); + }); + + describe('While polling pipeline data succesfully', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet('/dummy/endpoint').reply(() => { + const res = Promise.resolve([200, { + pipelines: [ + { + details: { + status: mockCiStatus, + }, + }, + ], + }]); + return res; + }); + vm = mountComponent(Component, { + endpoint: '/dummy/endpoint', + }); + }); + + afterEach(() => { + vm.poll.stop(); + vm.$destroy(); + mock.restore(); + }); + + it('shows the loading icon when polling is starting', (done) => { + expect(vm.$el.querySelector('.loading-container')).not.toBe(null); + setTimeout(() => { + expect(vm.$el.querySelector('.loading-container')).toBe(null); + done(); + }); + }); + + it('contains a ciStatus when the polling is succesful ', (done) => { + setTimeout(() => { + expect(vm.ciStatus).toEqual(mockCiStatus); + done(); + }); + }); + + it('contains a ci-status icon when polling is succesful', (done) => { + setTimeout(() => { + expect(vm.$el.querySelector('.ci-status-icon')).not.toBe(null); + expect(vm.$el.querySelector('.ci-status-icon').classList).toContain(`ci-status-icon-${mockCiStatus.group}`); + done(); + }); + }); + }); + + describe('When polling data was not succesful', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet('/dummy/endpoint').reply(() => { + const res = Promise.reject([502, { }]); + return res; + }); + vm = new Component({ + props: { + endpoint: '/dummy/endpoint', + }, + }); + }); + + afterEach(() => { + vm.poll.stop(); + vm.$destroy(); + mock.restore(); + }); + + it('calls an errorCallback', (done) => { + spyOn(vm, 'errorCallback').and.callThrough(); + vm.$mount(); + setTimeout(() => { + expect(vm.errorCallback.calls.count()).toEqual(1); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js index 481b46c3ac6..6fa6f44f953 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js @@ -1,7 +1,9 @@ /* eslint-disable no-new */ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; -import '~/flash'; +import timeoutPromise from './helpers/set_timeout_promise_helper'; describe('Mini Pipeline Graph Dropdown', () => { preloadFixtures('static/mini_dropdown_graph.html.raw'); @@ -27,6 +29,16 @@ describe('Mini Pipeline Graph Dropdown', () => { }); describe('When dropdown is clicked', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + it('should call getBuildsList', () => { const getBuildsListSpy = spyOn( MiniPipelineGraph.prototype, @@ -41,46 +53,55 @@ describe('Mini Pipeline Graph Dropdown', () => { }); it('should make a request to the endpoint provided in the html', () => { - const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {}); + const ajaxSpy = spyOn(axios, 'get').and.callThrough(); + + mock.onGet('foobar').reply(200, { + html: '', + }); new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); - expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar'); + expect(ajaxSpy.calls.allArgs()[0][0]).toEqual('foobar'); }); - it('should not close when user uses cmd/ctrl + click', () => { - spyOn($, 'ajax').and.callFake(function (params) { - params.success({ - html: `<li> - <a class="mini-pipeline-graph-dropdown-item" href="#"> - <span class="ci-status-icon ci-status-icon-failed"></span> - <span class="ci-build-text">build</span> - </a> - <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a> - </li>`, - }); + it('should not close when user uses cmd/ctrl + click', (done) => { + mock.onGet('foobar').reply(200, { + html: `<li> + <a class="mini-pipeline-graph-dropdown-item" href="#"> + <span class="ci-status-icon ci-status-icon-failed"></span> + <span class="ci-build-text">build</span> + </a> + <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a> + </li>`, }); new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); - document.querySelector('a.mini-pipeline-graph-dropdown-item').click(); - - expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true); + timeoutPromise() + .then(() => { + document.querySelector('a.mini-pipeline-graph-dropdown-item').click(); + }) + .then(timeoutPromise) + .then(() => { + expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true); + }) + .then(done) + .catch(done.fail); }); - }); - it('should close the dropdown when request returns an error', (done) => { - spyOn($, 'ajax').and.callFake(options => options.error()); + it('should close the dropdown when request returns an error', (done) => { + mock.onGet('foobar').networkError(); - new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); - document.querySelector('.js-builds-dropdown-button').click(); + document.querySelector('.js-builds-dropdown-button').click(); - setTimeout(() => { - expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false); - done(); - }, 0); + setTimeout(() => { + expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false); + done(); + }); + }); }); }); diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js index 2fd87754238..b09494f0b77 100644 --- a/spec/javascripts/pager_spec.js +++ b/spec/javascripts/pager_spec.js @@ -1,5 +1,6 @@ /* global fixture */ - +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import * as utils from '~/lib/utils/url_utility'; import Pager from '~/pager'; @@ -9,7 +10,6 @@ describe('pager', () => { beforeEach(() => { setFixtures('<div class="content_list"></div><div class="loading"></div>'); - spyOn($, 'ajax'); }); afterEach(() => { @@ -47,39 +47,90 @@ describe('pager', () => { }); describe('getOld', () => { + const urlRegex = /(.*)some_list(.*)$/; + let mock; + + function mockSuccess() { + mock.onGet(urlRegex).reply(200, { + count: 0, + html: '', + }); + } + + function mockError() { + mock.onGet(urlRegex).networkError(); + } + beforeEach(() => { setFixtures('<div class="content_list" data-href="/some_list"></div><div class="loading"></div>'); + spyOn(axios, 'get').and.callThrough(); + + mock = new MockAdapter(axios); + Pager.init(); }); - it('shows loader while loading next page', () => { + afterEach(() => { + mock.restore(); + }); + + it('shows loader while loading next page', (done) => { + mockSuccess(); + spyOn(Pager.loading, 'show'); Pager.getOld(); - expect(Pager.loading.show).toHaveBeenCalled(); + + setTimeout(() => { + expect(Pager.loading.show).toHaveBeenCalled(); + + done(); + }); }); - it('hides loader on success', () => { - spyOn($, 'ajax').and.callFake(options => options.success({})); + it('hides loader on success', (done) => { + mockSuccess(); + spyOn(Pager.loading, 'hide'); Pager.getOld(); - expect(Pager.loading.hide).toHaveBeenCalled(); + + setTimeout(() => { + expect(Pager.loading.hide).toHaveBeenCalled(); + + done(); + }); }); - it('hides loader on error', () => { - spyOn($, 'ajax').and.callFake(options => options.error()); + it('hides loader on error', (done) => { + mockError(); + spyOn(Pager.loading, 'hide'); Pager.getOld(); - expect(Pager.loading.hide).toHaveBeenCalled(); + + setTimeout(() => { + expect(Pager.loading.hide).toHaveBeenCalled(); + + done(); + }); }); - it('sends request to url with offset and limit params', () => { - spyOn($, 'ajax'); + it('sends request to url with offset and limit params', (done) => { Pager.offset = 100; Pager.limit = 20; Pager.getOld(); - const [{ data, url }] = $.ajax.calls.argsFor(0); - expect(data).toBe('limit=20&offset=100'); - expect(url).toBe('/some_list'); + + setTimeout(() => { + const [url, params] = axios.get.calls.argsFor(0); + + expect(params).toEqual({ + params: { + limit: 20, + offset: 100, + }, + }); + expect(url).toBe('/some_list'); + + done(); + }); }); }); }); diff --git a/spec/javascripts/vue_shared/components/confirmation_input_spec.js b/spec/javascripts/vue_shared/components/confirmation_input_spec.js new file mode 100644 index 00000000000..a6a12614e77 --- /dev/null +++ b/spec/javascripts/vue_shared/components/confirmation_input_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; +import confirmationInput from '~/vue_shared/components/confirmation_input.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Confirmation input component', () => { + const Component = Vue.extend(confirmationInput); + const props = { + inputId: 'dummy-id', + confirmationKey: 'confirmation-key', + confirmationValue: 'confirmation-value', + }; + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('props', () => { + beforeEach(() => { + vm = mountComponent(Component, props); + }); + + it('sets id of the input field to inputId', () => { + expect(vm.$refs.enteredValue.id).toBe(props.inputId); + }); + + it('sets name of the input field to confirmationKey', () => { + expect(vm.$refs.enteredValue.name).toBe(props.confirmationKey); + }); + }); + + describe('computed', () => { + describe('inputLabel', () => { + it('escapes confirmationValue by default', () => { + vm = mountComponent(Component, { ...props, confirmationValue: 'n<e></e>ds escap"ng' }); + expect(vm.inputLabel).toBe('Type <code>n<e></e>ds escap"ng</code> to confirm:'); + }); + + it('does not escape confirmationValue if escapeValue is false', () => { + vm = mountComponent(Component, { ...props, confirmationValue: 'n<e></e>ds escap"ng', shouldEscapeConfirmationValue: false }); + expect(vm.inputLabel).toBe('Type <code>n<e></e>ds escap"ng</code> to confirm:'); + }); + }); + }); + + describe('methods', () => { + describe('hasCorrectValue', () => { + beforeEach(() => { + vm = mountComponent(Component, props); + }); + + it('returns false if entered value is incorrect', () => { + vm.$refs.enteredValue.value = 'incorrect'; + expect(vm.hasCorrectValue()).toBe(false); + }); + + it('returns true if entered value is correct', () => { + vm.$refs.enteredValue.value = props.confirmationValue; + expect(vm.hasCorrectValue()).toBe(true); + }); + }); + }); +}); diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index d4f56a41d9a..8e0ebb1f6fa 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1752,6 +1752,44 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#write_config' do + before do + repository.rugged.config["gitlab.fullpath"] = repository.path + end + + shared_examples 'writing repo config' do + context 'is given a path' do + it 'writes it to disk' do + repository.write_config(full_path: "not-the/real-path.git") + + config = File.read(File.join(repository.path, "config")) + + expect(config).to include("[gitlab]") + expect(config).to include("fullpath = not-the/real-path.git") + end + end + + context 'it is given an empty path' do + it 'does not write it to disk' do + repository.write_config(full_path: "") + + config = File.read(File.join(repository.path, "config")) + + expect(config).to include("[gitlab]") + expect(config).to include("fullpath = #{repository.path}") + end + end + end + + context "when gitaly_write_config is enabled" do + it_behaves_like "writing repo config" + end + + context "when gitaly_write_config is disabled", :disable_gitaly do + it_behaves_like "writing repo config" + end + end + describe '#merge' do let(:repository) do Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') diff --git a/spec/lib/gitlab/slash_commands/issue_search_spec.rb b/spec/lib/gitlab/slash_commands/issue_search_spec.rb index e41e5254dde..35d01efc1bd 100644 --- a/spec/lib/gitlab/slash_commands/issue_search_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_search_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::SlashCommands::IssueSearch do let!(:issue) { create(:issue, project: project, title: 'find me') } let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') } let(:project) { create(:project) } - let(:user) { issue.author } + let(:user) { create(:user) } let(:regex_match) { described_class.match("issue search find") } subject do diff --git a/spec/policies/ci/pipeline_schedule_policy_spec.rb b/spec/policies/ci/pipeline_schedule_policy_spec.rb index 1b0e9fac355..c0c3eda4911 100644 --- a/spec/policies/ci/pipeline_schedule_policy_spec.rb +++ b/spec/policies/ci/pipeline_schedule_policy_spec.rb @@ -88,5 +88,19 @@ describe Ci::PipelineSchedulePolicy, :models do expect(policy).to be_allowed :admin_pipeline_schedule end end + + describe 'rules for non-owner of schedule' do + let(:owner) { create(:user) } + + before do + project.add_master(owner) + project.add_master(user) + pipeline_schedule.update(owner: owner) + end + + it 'includes abilities to take ownership' do + expect(policy).to be_allowed :take_ownership_pipeline_schedule + end + end end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index f2593a1a75c..129344f105f 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -92,7 +92,7 @@ describe ProjectPolicy do it 'does not include the read_issue permission when the issue author is not a member of the private project' do project = create(:project, :private) - issue = create(:issue, project: project) + issue = create(:issue, project: project, author: create(:user)) user = issue.author expect(project.team.member?(issue.author)).to be false diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 8897a64a138..47c1ebbeb81 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -4,7 +4,7 @@ describe Issues::CloseService do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest) { create(:user) } - let(:issue) { create(:issue, assignees: [user2]) } + let(:issue) { create(:issue, assignees: [user2], author: create(:user)) } let(:project) { issue.project } let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 1cb6f2e097f..41237dd7160 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -13,7 +13,8 @@ describe Issues::UpdateService, :mailer do create(:issue, title: 'Old title', description: "for #{user2.to_reference}", assignee_ids: [user3.id], - project: project) + project: project, + author: create(:user)) end before do diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 4d12de3ecce..216e0cd4266 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -4,7 +4,7 @@ describe MergeRequests::CloseService do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest) { create(:user) } - let(:merge_request) { create(:merge_request, assignee: user2) } + let(:merge_request) { create(:merge_request, assignee: user2, author: create(:user)) } let(:project) { merge_request.project } let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) } diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb index aa90feeef89..5ef6365fcc9 100644 --- a/spec/services/merge_requests/ff_merge_service_spec.rb +++ b/spec/services/merge_requests/ff_merge_service_spec.rb @@ -7,7 +7,8 @@ describe MergeRequests::FfMergeService do create(:merge_request, source_branch: 'flatten-dir', target_branch: 'improve/awesome', - assignee: user2) + assignee: user2, + author: create(:user)) end let(:project) { merge_request.project } diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index a44d63e5f9f..9ee37c51d95 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -4,7 +4,7 @@ describe MergeRequests::ReopenService do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest) { create(:user) } - let(:merge_request) { create(:merge_request, :closed, assignee: user2) } + let(:merge_request) { create(:merge_request, :closed, assignee: user2, author: create(:user)) } let(:project) { merge_request.project } before do diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 2238da2d14d..c31259239ee 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -12,7 +12,8 @@ describe MergeRequests::UpdateService, :mailer do create(:merge_request, :simple, title: 'Old title', description: "FYI #{user2.to_reference}", assignee_id: user3.id, - source_project: project) + source_project: project, + author: create(:user)) end before do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 5c59455e3e1..35eb84e5e88 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -458,7 +458,7 @@ describe NotificationService, :mailer do context "merge request diff note" do let(:project) { create(:project, :repository) } let(:user) { create(:user) } - let(:merge_request) { create(:merge_request, source_project: project, assignee: user) } + let(:merge_request) { create(:merge_request, source_project: project, assignee: user, author: create(:user)) } let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) } before do @@ -469,11 +469,13 @@ describe NotificationService, :mailer do describe '#new_note' do it "records sent notifications" do - # Ensure create SentNotification by noteable = merge_request 6 times, not noteable = note + # 3 SentNotification are sent: the MR assignee and author, and the @u_watcher expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(3).times.and_call_original notification.new_note(note) + expect(SentNotification.last(3).map(&:recipient).map(&:id)) + .to contain_exactly(merge_request.assignee.id, merge_request.author.id, @u_watcher.id) expect(SentNotification.last.in_reply_to_discussion_id).to eq(note.discussion_id) end end diff --git a/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb b/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb new file mode 100644 index 00000000000..6e7d8db99c4 --- /dev/null +++ b/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe 'projects/pipeline_schedules/_pipeline_schedule' do + let(:owner) { create(:user) } + let(:master) { create(:user) } + let(:project) { create(:project) } + let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) } + + before do + assign(:project, project) + + allow(view).to receive(:current_user).and_return(user) + allow(view).to receive(:pipeline_schedule).and_return(pipeline_schedule) + + allow(view).to receive(:can?).and_return(true) + end + + context 'taking ownership of schedule' do + context 'when non-owner is signed in' do + let(:user) { master } + + before do + allow(view).to receive(:can?).with(master, :take_ownership_pipeline_schedule, pipeline_schedule).and_return(true) + end + + it 'non-owner can take ownership of pipeline' do + render + + expect(rendered).to have_link('Take ownership') + end + end + + context 'when owner is signed in' do + let(:user) { owner } + + before do + allow(view).to receive(:can?).with(owner, :take_ownership_pipeline_schedule, pipeline_schedule).and_return(false) + end + + it 'owner cannot take ownership of pipeline' do + render + + expect(rendered).not_to have_link('Take ownership') + end + end + end +end |