Commit 26debb12 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into 'zj-ref-contains'

# Conflicts:
#   lib/gitlab/git/repository.rb
parents fd46d6ce cf644fc1
...@@ -738,8 +738,9 @@ cache gems: ...@@ -738,8 +738,9 @@ cache gems:
gitlab_git_test: gitlab_git_test:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs-and-qa <<: *except-docs-and-qa
<<: *pull-cache
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
before_script: []
cache: {}
script: script:
- spec/support/prepare-gitlab-git-test-for-commit --check-for-changes - spec/support/prepare-gitlab-git-test-for-commit --check-for-changes
/* eslint-disable no-new */ /* 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. * In each pipelines table we have a mini pipeline graph for each pipeline.
...@@ -78,27 +79,22 @@ export default class MiniPipelineGraph { ...@@ -78,27 +79,22 @@ export default class MiniPipelineGraph {
const button = e.relatedTarget; const button = e.relatedTarget;
const endpoint = button.dataset.stageEndpoint; const endpoint = button.dataset.stageEndpoint;
return $.ajax({ this.renderBuildsList(button, '');
dataType: 'json', this.toggleLoading(button);
type: 'GET',
url: endpoint, axios.get(endpoint)
beforeSend: () => { .then(({ data }) => {
this.renderBuildsList(button, '');
this.toggleLoading(button);
},
success: (data) => {
this.toggleLoading(button); this.toggleLoading(button);
this.renderBuildsList(button, data.html); this.renderBuildsList(button, data.html);
this.stopDropdownClickPropagation(); this.stopDropdownClickPropagation();
}, })
error: () => { .catch(() => {
this.toggleLoading(button); this.toggleLoading(button);
if ($(button).parent().hasClass('open')) { if ($(button).parent().hasClass('open')) {
$(button).dropdown('toggle'); $(button).dropdown('toggle');
} }
new Flash('An error occurred while fetching the builds.', 'alert'); flash('An error occurred while fetching the builds.', 'alert');
}, });
});
} }
/** /**
......
/* 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 */ /* 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'; import Raphael from './raphael';
export default (function() { export default (function() {
...@@ -26,16 +29,13 @@ export default (function() { ...@@ -26,16 +29,13 @@ export default (function() {
} }
BranchGraph.prototype.load = function() { BranchGraph.prototype.load = function() {
return $.ajax({ axios.get(this.options.url)
url: this.options.url, .then(({ data }) => {
method: "get",
dataType: "json",
success: $.proxy(function(data) {
$(".loading", this.element).hide(); $(".loading", this.element).hide();
this.prepareData(data.days, data.commits); this.prepareData(data.days, data.commits);
return this.buildGraph(); this.buildGraph();
}, this) })
}); .catch(() => __('Error fetching network graph.'));
}; };
BranchGraph.prototype.prepareData = function(days, commits) { BranchGraph.prototype.prepareData = function(days, commits) {
......
...@@ -16,6 +16,7 @@ import Autosize from 'autosize'; ...@@ -16,6 +16,7 @@ import Autosize from 'autosize';
import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho'; import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache'; import AjaxCache from '~/lib/utils/ajax_cache';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility'; import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash'; import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle'; import CommentTypeToggle from './comment_type_toggle';
...@@ -252,26 +253,20 @@ export default class Notes { ...@@ -252,26 +253,20 @@ export default class Notes {
return; return;
} }
this.refreshing = true; this.refreshing = true;
return $.ajax({ axios.get(this.notes_url, {
url: this.notes_url, headers: {
headers: { 'X-Last-Fetched-At': this.last_fetched_at }, 'X-Last-Fetched-At': this.last_fetched_at,
dataType: 'json', },
success: (function(_this) { }).then(({ data }) => {
return function(data) { const notes = data.notes;
var notes; this.last_fetched_at = data.last_fetched_at;
notes = data.notes; this.setPollingInterval(data.notes.length);
_this.last_fetched_at = data.last_fetched_at; $.each(notes, (i, note) => this.renderNote(note));
_this.setPollingInterval(data.notes.length);
return $.each(notes, function(i, note) { this.refreshing = false;
_this.renderNote(note); }).catch(() => {
}); this.refreshing = false;
}; });
})(this)
}).always((function(_this) {
return function() {
return _this.refreshing = false;
};
})(this));
} }
/** /**
......
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
export default class NotificationsForm { export default class NotificationsForm {
constructor() { constructor() {
this.toggleCheckbox = this.toggleCheckbox.bind(this); this.toggleCheckbox = this.toggleCheckbox.bind(this);
...@@ -27,24 +31,20 @@ export default class NotificationsForm { ...@@ -27,24 +31,20 @@ export default class NotificationsForm {
saveEvent($checkbox, $parent) { saveEvent($checkbox, $parent) {
const form = $parent.parents('form:first'); const form = $parent.parents('form:first');
return $.ajax({ this.showCheckboxLoadingSpinner($parent);
url: form.attr('action'),
method: form.attr('method'), axios[form.attr('method')](form.attr('action'), form.serialize())
dataType: 'json', .then(({ data }) => {
data: form.serialize(), $checkbox.enable();
beforeSend: () => { if (data.saved) {
this.showCheckboxLoadingSpinner($parent); $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done');
}, setTimeout(() => {
}).done((data) => { $parent.removeClass('is-loading')
$checkbox.enable(); .find('.custom-notification-event-loading')
if (data.saved) { .toggleClass('fa-spin fa-spinner fa-check is-done');
$parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); }, 2000);
setTimeout(() => { }
$parent.removeClass('is-loading') })
.find('.custom-notification-event-loading') .catch(() => flash(__('There was an error saving your notification settings.')));
.toggleClass('fa-spin fa-spinner fa-check is-done');
}, 2000);
}
});
} }
} }
import { getParameterByName } from '~/lib/utils/common_utils'; import { getParameterByName } from '~/lib/utils/common_utils';
import axios from './lib/utils/axios_utils';
import { removeParams } from './lib/utils/url_utility'; import { removeParams } from './lib/utils/url_utility';
const ENDLESS_SCROLL_BOTTOM_PX = 400; const ENDLESS_SCROLL_BOTTOM_PX = 400;
...@@ -22,24 +23,22 @@ export default { ...@@ -22,24 +23,22 @@ export default {
getOld() { getOld() {
this.loading.show(); this.loading.show();
$.ajax({ axios.get(this.url, {
type: 'GET', params: {
url: this.url, limit: this.limit,
data: `limit=${this.limit}&offset=${this.offset}`, 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();
}
}, },
}); }).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) { append(count, html) {
......
import axios from '../../../lib/utils/axios_utils';
import { __ } from '../../../locale';
import flash from '../../../flash';
export default function UsagePing() { export default function UsagePing() {
const usageDataUrl = $('.usage-data').data('endpoint'); const el = document.querySelector('.usage-data');
$.ajax({ axios.get(el.dataset.endpoint, {
type: 'GET', responseType: 'text',
url: usageDataUrl, }).then(({ data }) => {
dataType: 'html', el.innerHTML = data;
success(html) { }).catch(() => flash(__('Error fetching usage ping data.')));
$('.usage-data').html(html);
},
});
} }
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import UsersSelect from '~/users_select'; import UsersSelect from '~/users_select';
import { isMetaClick } from '~/lib/utils/common_utils'; 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 { export default class Todos {
constructor() { constructor() {
...@@ -59,18 +62,12 @@ export default class Todos { ...@@ -59,18 +62,12 @@ export default class Todos {
const target = e.target; const target = e.target;
target.setAttribute('disabled', true); target.setAttribute('disabled', true);
target.classList.add('disabled'); target.classList.add('disabled');
$.ajax({
type: 'POST', axios[target.dataset.method](target.dataset.href)
url: target.dataset.href, .then(({ data }) => {
dataType: 'json',
data: {
'_method': target.dataset.method,
},
success: (data) => {
this.updateRowState(target); this.updateRowState(target);
return this.updateBadges(data); this.updateBadges(data);
}, }).catch(() => flash(__('Error updating todo status.')));
});
} }
updateRowState(target) { updateRowState(target) {
...@@ -98,19 +95,15 @@ export default class Todos { ...@@ -98,19 +95,15 @@ export default class Todos {
e.preventDefault(); e.preventDefault();
const target = e.currentTarget; const target = e.currentTarget;
const requestData = { '_method': target.dataset.method, ids: this.todo_ids };
target.setAttribute('disabled', true); target.setAttribute('disabled', true);
target.classList.add('disabled'); target.classList.add('disabled');
$.ajax({
type: 'POST', axios[target.dataset.method](target.dataset.href, {
url: target.dataset.href, ids: this.todo_ids,
dataType: 'json', }).then(({ data }) => {
data: requestData, this.updateAllState(target, data);
success: (data) => { this.updateBadges(data);
this.updateAllState(target, data); }).catch(() => flash(__('Error updating status for all todos.')));
return this.updateBadges(data);
},
});
} }
updateAllState(target, data) { updateAllState(target, data) {
......
import Vue from 'vue';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import TreeView from '../../../../tree'; import TreeView from '../../../../tree';
import ShortcutsNavigation from '../../../../shortcuts_navigation'; import ShortcutsNavigation from '../../../../shortcuts_navigation';
import BlobViewer from '../../../../blob/viewer'; import BlobViewer from '../../../../blob/viewer';
...@@ -11,5 +13,25 @@ export default () => { ...@@ -11,5 +13,25 @@ export default () => {
new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new
$('#tree-slider').waitForImages(() => $('#tree-slider').waitForImages(() =>
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath)); 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,
},
});
},
});
}
}; };
<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>
import axios from '~/lib/utils/axios_utils';
export default class CommitPipelineService {
constructor(endpoint) {
this.endpoint = endpoint;
}
fetchData() {
return axios.get(this.endpoint);
}
}
<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>
...@@ -195,6 +195,18 @@ ...@@ -195,6 +195,18 @@
.commit-actions { .commit-actions {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
font-size: 0; font-size: 0;
div {
display: inline;
}
.fa-spinner {
font-size: 12px;
}
span {
font-size: 6px;
}
} }
.ci-status-link { .ci-status-link {
...@@ -219,6 +231,11 @@ ...@@ -219,6 +231,11 @@
font-size: 14px; font-size: 14px;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
.ci-status-icon {
position: relative;
top: 1px;
}
} }
.commit, .commit,
......
...@@ -10,6 +10,10 @@ module Ci ...@@ -10,6 +10,10 @@ module Ci
can?(:developer_access) && pipeline_schedule.owned_by?(@user) can?(:developer_access) && pipeline_schedule.owned_by?(@user)
end end
condition(:non_owner_of_schedule) do
!pipeline_schedule.owned_by?(@user)
end
rule { can?(:developer_access) }.policy do rule { can?(:developer_access) }.policy do
enable :play_pipeline_schedule enable :play_pipeline_schedule
end end
...@@ -19,6 +23,10 @@ module Ci ...@@ -19,6 +23,10 @@ module Ci
enable :admin_pipeline_schedule enable :admin_pipeline_schedule
end end
rule { can?(:master_access) & non_owner_of_schedule }.policy do
enable :take_ownership_pipeline_schedule
end
rule { protected_ref }.prevent :play_pipeline_schedule rule { protected_ref }.prevent :play_pipeline_schedule
end end
end end
...@@ -51,6 +51,7 @@ ...@@ -51,6 +51,7 @@
- if commit.status(ref) - if commit.status(ref)
= render_commit_status(commit, ref: 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" = 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")) = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"))
= link_to_browse_code(project, commit) = link_to_browse_code(project, commit)
......
...@@ -29,9 +29,10 @@ ...@@ -29,9 +29,10 @@
- if can?(current_user, :play_pipeline_schedule, pipeline_schedule) - 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 = link_to play_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('Play'), class: 'btn' do
= icon('play') = 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 = link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn' do
= s_('PipelineSchedules|Take ownership') = 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 = link_to edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), class: 'btn' do
= icon('pencil') = icon('pencil')
- if can?(current_user, :admin_pipeline_schedule, pipeline_schedule) - if can?(current_user, :admin_pipeline_schedule, pipeline_schedule)
......
---
title: Hide pipeline schedule take ownership for current owner
merge_request: 12986
author:
type: fixed
---
title: Add realtime ci status for the repository -> files view
merge_request: 16523
author:
type: added
---
title: Add confirmation-input component
merge_request: 16816
author:
type: other
...@@ -8,23 +8,13 @@ comments: false ...@@ -8,23 +8,13 @@ comments: false
Welcome to [GitLab](https://about.gitlab.com/), a Git-based fully featured Welcome to [GitLab](https://about.gitlab.com/), a Git-based fully featured
platform for software development! 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/), 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.
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).
> **GitLab EE** contains all features available in **GitLab CE**, 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.
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**.
---- ## Shortcuts to GitLab's most visited docs
Shortcuts to GitLab's most visited docs:
| [GitLab CI/CD](ci/README.md) | Other | | [GitLab CI/CD](ci/README.md) | Other |
| :----- | :----- | | :----- | :----- |
...@@ -134,14 +124,8 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i ...@@ -134,14 +124,8 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
## Administrator documentation ## Administrator documentation
[Administration documentation](administration/index.md) applies to admin users of GitLab [Administration documentation](administration/index.md) applies to admin users of [GitLab
self-hosted instances: self-hosted instances](#self-hosted-gitlab): Libre, Starter, Premium, Ultimate.
- GitLab Community Edition
- GitLab [Enterprise Editions](https://about.gitlab.com/gitlab-ee/)
- Enterprise Edition Starter (EES)
- Enterprise Edition Premium (EEP)
- Enterprise Edition Ultimate (EEU)
Learn how to install, configure, update, upgrade, integrate, and maintain your own instance. 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. Regular users don't have access to GitLab administration tools and settings.
......
...@@ -5,17 +5,32 @@ Enterprise Edition (look for the [`CE Upstream` merge requests]). ...@@ -5,17 +5,32 @@ Enterprise Edition (look for the [`CE Upstream` merge requests]).
This merge is done automatically in a This merge is done automatically in a
[scheduled pipeline](https://gitlab.com/gitlab-org/release-tools/-/jobs/43201679). [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, ## What to do 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!**
1. 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.
It's ok to resolve more conflicts than the one that you are asked to resolve. In In that case, it's a good habit to ask for a double-check on your resolution
that case, it's a good habit to ask for a double-check on your resolution by by someone who is familiar with the code you touched.
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 [`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 ## Always merge EE merge requests before their CE counterparts
......
...@@ -90,7 +90,8 @@ structure. ...@@ -90,7 +90,8 @@ structure.
To create a subgroup: 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) ![Subgroups page](img/create_subgroup_button.png)
......
...@@ -6,6 +6,7 @@ module Gitlab ...@@ -6,6 +6,7 @@ module Gitlab
CommandError = Class.new(StandardError) CommandError = Class.new(StandardError)
CommitError = Class.new(StandardError) CommitError = Class.new(StandardError)
OSError = Class.new(StandardError)
class << self class << self
include Gitlab::EncodingHelper include Gitlab::EncodingHelper
......
...@@ -1306,7 +1306,15 @@ module Gitlab ...@@ -1306,7 +1306,15 @@ module Gitlab
# rubocop:enable Metrics/ParameterLists # rubocop:enable Metrics/ParameterLists
def write_config(full_path:) 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 end
def gitaly_repository def gitaly_repository
...@@ -1464,6 +1472,10 @@ module Gitlab ...@@ -1464,6 +1472,10 @@ module Gitlab
names names
end end
def rugged_write_config(full_path:)
rugged.config['gitlab.fullpath'] = full_path
end
def shell_write_ref(ref_path, ref, old_ref) 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_path #{ref_path.inspect}" if ref_path.include?(' ')
raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00") raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00")
......
...@@ -219,6 +219,19 @@ module Gitlab ...@@ -219,6 +219,19 @@ module Gitlab
true true
end 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 end
end end
#!/usr/bin/env ruby #!/usr/bin/env ruby
ALLOWED = [ 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', 'config/initializers/8_metrics.rb',
# Can be deleted once wiki's are fully (mandatory) migrated # Can be deleted once wiki's are fully (mandatory) migrated
...@@ -13,9 +13,6 @@ ALLOWED = [ ...@@ -13,9 +13,6 @@ ALLOWED = [
# Needs to be migrated, https://gitlab.com/gitlab-org/gitaly/issues/954 # Needs to be migrated, https://gitlab.com/gitlab-org/gitaly/issues/954
'lib/tasks/gitlab/cleanup.rake', '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 # The only place where Rugged code is still allowed in production
'lib/gitlab/git/' 'lib/gitlab/git/'
].freeze ].freeze
......
...@@ -23,7 +23,7 @@ FactoryBot.define do ...@@ -23,7 +23,7 @@ FactoryBot.define do
end end
after(:build) do |commit, evaluator| 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 end
trait :without_author do trait :without_author do
......
...@@ -3,13 +3,14 @@ FactoryBot.define do ...@@ -3,13 +3,14 @@ FactoryBot.define do
sha '97de212e80737a608d939f648d959671fb0a0142' sha '97de212e80737a608d939f648d959671fb0a0142'
ref 'master' ref 'master'
tag false tag false
user user nil
project nil project nil
deployable factory: :ci_build deployable factory: :ci_build
environment factory: :environment environment factory: :environment
after(:build) do |deployment, evaluator| after(:build) do |deployment, evaluator|
deployment.project ||= deployment.environment.project deployment.project ||= deployment.environment.project
deployment.user ||= deployment.project.creator
unless deployment.project.repository_exists? unless deployment.project.repository_exists?
allow(deployment.project.repository).to receive(:create_ref) allow(deployment.project.repository).to receive(:create_ref)
......
FactoryBot.define do FactoryBot.define do
factory :event do factory :event do
project project
author factory: :user author(factory: :user) { project.creator }
action Event::JOINED action Event::JOINED
trait(:created) { action Event::CREATED } trait(:created) { action Event::CREATED }
......
FactoryBot.define do FactoryBot.define do
factory :issue do factory :issue do
title { generate(:title) } title { generate(:title) }
author
project project
author { project.creator }
trait :confidential do trait :confidential do
confidential true confidential true
......
FactoryBot.define do FactoryBot.define do
factory :merge_request do factory :merge_request do
title { generate(:title) } title { generate(:title) }
author
association :source_project, :repository, factory: :project association :source_project, :repository, factory: :project
target_project { source_project } target_project { source_project }
author { source_project.creator }
# $ git log --pretty=oneline feature..master # $ git log --pretty=oneline feature..master
# 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com # 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com
......
...@@ -6,7 +6,7 @@ FactoryBot.define do ...@@ -6,7 +6,7 @@ FactoryBot.define do
factory :note do factory :note do
project project
note { generate(:title) } note { generate(:title) }
author author { project&.creator || create(:user) }
on_issue on_issue
factory :note_on_commit, traits: [:on_commit] factory :note_on_commit, traits: [:on_commit]
......
...@@ -3,7 +3,7 @@ FactoryBot.define do ...@@ -3,7 +3,7 @@ FactoryBot.define do
skip_create skip_create
project project
user factory: :user user { project.creator }
initialize_with { new(project, user) } initialize_with { new(project, user) }
end end
end end
FactoryBot.define do FactoryBot.define do
factory :sent_notification do factory :sent_notification do
project project
recipient factory: :user recipient { project.creator }
noteable { create(:issue, project: project) } noteable { create(:issue, project: project) }
reply_key { SentNotification.reply_key } reply_key { SentNotification.reply_key }
end end
......
...@@ -21,6 +21,7 @@ FactoryBot.define do ...@@ -21,6 +21,7 @@ FactoryBot.define do
factory :project_snippet, parent: :snippet, class: :ProjectSnippet do factory :project_snippet, parent: :snippet, class: :ProjectSnippet do
project project
author { project.creator }
end end
factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do
......
FactoryBot.define do FactoryBot.define do
factory :subscription do factory :subscription do
user
project project
user { project.creator }
subscribable factory: :issue subscribable factory: :issue
end end
end end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
FactoryBot.define do FactoryBot.define do
factory :timelog do factory :timelog do
time_spent 3600 time_spent 3600
user
issue issue
user { issue.project.creator }
end end
end end
FactoryBot.define do FactoryBot.define do
factory :todo do factory :todo do
project project
author author { project.creator }
user user { project.creator }
target factory: :issue target factory: :issue
action { Todo::ASSIGNED } action { Todo::ASSIGNED }
......
...@@ -44,36 +44,38 @@ feature 'Dashboard Merge Requests' do ...@@ -44,36 +44,38 @@ feature 'Dashboard Merge Requests' do
context 'merge requests exist' do context 'merge requests exist' do
let!(:assigned_merge_request) 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 end
let!(:assigned_merge_request_from_fork) do let!(:assigned_merge_request_from_fork) do
create(:merge_request, create(:merge_request,
source_branch: 'markdown', assignee: current_user, 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 end
let!(:authored_merge_request) do let!(:authored_merge_request) do
create(:merge_request, create(:merge_request,
source_branch: 'markdown', author: current_user, source_branch: 'markdown',
target_project: project, source_project: project source_project: project,
) author: current_user)
end end
let!(:authored_merge_request_from_fork) do let!(:authored_merge_request_from_fork) do
create(:merge_request, create(:merge_request,
source_branch: 'feature_conflict', source_branch: 'feature_conflict',
author: current_user, author: current_user,
target_project: public_project, source_project: forked_project target_project: public_project, source_project: forked_project)
)
end end
let!(:other_merge_request) do let!(:other_merge_request) do
create(:merge_request, create(:merge_request,
source_branch: 'fix', source_branch: 'fix',
target_project: project, source_project: project source_project: project,
) author: create(:user))
end end
before do before do
......
...@@ -3,7 +3,7 @@ require 'rails_helper' ...@@ -3,7 +3,7 @@ require 'rails_helper'
describe 'Merge request > User awards emoji', :js do describe 'Merge request > User awards emoji', :js do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator } 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 describe 'logged in' do
before do before do
......
...@@ -2,12 +2,16 @@ ...@@ -2,12 +2,16 @@
"type": ["object", "null"], "type": ["object", "null"],
"required": [ "required": [
"id", "id",
"name",
"username",
"state", "state",
"avatar_url", "avatar_url",
"web_url" "web_url"
], ],
"properties": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"name": { "type": "string" },
"username": { "type": "string" },
"state": { "type": "string" }, "state": { "type": "string" },
"avatar_url": { "type": "string" }, "avatar_url": { "type": "string" },
"web_url": { "type": "string" } "web_url": { "type": "string" }
......
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();
});
});
});
});
/* eslint-disable no-new */ /* 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 MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import '~/flash'; import timeoutPromise from './helpers/set_timeout_promise_helper';
describe('Mini Pipeline Graph Dropdown', () => { describe('Mini Pipeline Graph Dropdown', () => {
preloadFixtures('static/mini_dropdown_graph.html.raw'); preloadFixtures('static/mini_dropdown_graph.html.raw');
...@@ -27,6 +29,16 @@ describe('Mini Pipeline Graph Dropdown', () => { ...@@ -27,6 +29,16 @@ describe('Mini Pipeline Graph Dropdown', () => {
}); });
describe('When dropdown is clicked', () => { describe('When dropdown is clicked', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
it('should call getBuildsList', () => { it('should call getBuildsList', () => {
const getBuildsListSpy = spyOn( const getBuildsListSpy = spyOn(
MiniPipelineGraph.prototype, MiniPipelineGraph.prototype,
...@@ -41,46 +53,55 @@ describe('Mini Pipeline Graph Dropdown', () => { ...@@ -41,46 +53,55 @@ describe('Mini Pipeline Graph Dropdown', () => {
}); });
it('should make a request to the endpoint provided in the html', () => { 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(); new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click(); 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', () => { it('should not close when user uses cmd/ctrl + click', (done) => {
spyOn($, 'ajax').and.callFake(function (params) { mock.onGet('foobar').reply(200, {
params.success({ html: `<li>
html: `<li> <a class="mini-pipeline-graph-dropdown-item" href="#">
<a class="mini-pipeline-graph-dropdown-item" href="#"> <span class="ci-status-icon ci-status-icon-failed"></span>
<span class="ci-status-icon ci-status-icon-failed"></span> <span class="ci-build-text">build</span>
<span class="ci-build-text">build</span> </a>
</a> <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a>
<a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a> </li>`,
</li>`,
});
}); });
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();
document.querySelector('a.mini-pipeline-graph-dropdown-item').click(); timeoutPromise()
.then(() => {
expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true); 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) => { it('should close the dropdown when request returns an error', (done) => {
spyOn($, 'ajax').and.callFake(options => options.error()); 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(() => { setTimeout(() => {
expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false); expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false);
done(); done();
}, 0); });
});
}); });
}); });
/* global fixture */ /* global fixture */
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import * as utils from '~/lib/utils/url_utility'; import * as utils from '~/lib/utils/url_utility';
import Pager from '~/pager'; import Pager from '~/pager';
...@@ -9,7 +10,6 @@ describe('pager', () => { ...@@ -9,7 +10,6 @@ describe('pager', () => {
beforeEach(() => { beforeEach(() => {
setFixtures('<div class="content_list"></div><div class="loading"></div>'); setFixtures('<div class="content_list"></div><div class="loading"></div>');
spyOn($, 'ajax');
}); });
afterEach(() => { afterEach(() => {
...@@ -47,39 +47,90 @@ describe('pager', () => { ...@@ -47,39 +47,90 @@ describe('pager', () => {
}); });
describe('getOld', () => { 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(() => { beforeEach(() => {
setFixtures('<div class="content_list" data-href="/some_list"></div><div class="loading"></div>'); 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(); 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'); spyOn(Pager.loading, 'show');
Pager.getOld(); Pager.getOld();
expect(Pager.loading.show).toHaveBeenCalled();
setTimeout(() => {
expect(Pager.loading.show).toHaveBeenCalled();
done();
});
}); });
it('hides loader on success', () => { it('hides loader on success', (done) => {
spyOn($, 'ajax').and.callFake(options => options.success({})); mockSuccess();
spyOn(Pager.loading, 'hide'); spyOn(Pager.loading, 'hide');
Pager.getOld(); Pager.getOld();
expect(Pager.loading.hide).toHaveBeenCalled();
setTimeout(() => {
expect(Pager.loading.hide).toHaveBeenCalled();
done();
});
}); });
it('hides loader on error', () => { it('hides loader on error', (done) => {
spyOn($, 'ajax').and.callFake(options => options.error()); mockError();
spyOn(Pager.loading, 'hide'); spyOn(Pager.loading, 'hide');
Pager.getOld(); Pager.getOld();
expect(Pager.loading.hide).toHaveBeenCalled();
setTimeout(() => {
expect(Pager.loading.hide).toHaveBeenCalled();
done();
});
}); });
it('sends request to url with offset and limit params', () => { it('sends request to url with offset and limit params', (done) => {
spyOn($, 'ajax');
Pager.offset = 100; Pager.offset = 100;
Pager.limit = 20; Pager.limit = 20;
Pager.getOld(); Pager.getOld();
const [{ data, url }] = $.ajax.calls.argsFor(0);
expect(data).toBe('limit=20&offset=100'); setTimeout(() => {
expect(url).toBe('/some_list'); const [url, params] = axios.get.calls.argsFor(0);
expect(params).toEqual({
params: {
limit: 20,
offset: 100,
},
});
expect(url).toBe('/some_list');
done();
});
}); });
}); });
}); });
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&lt;e&gt;&lt;/e&gt;ds escap&quot;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);
});
});
});
});
...@@ -1752,6 +1752,44 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -1752,6 +1752,44 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
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 describe '#merge' do
let(:repository) do let(:repository) do
Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
......
...@@ -5,7 +5,7 @@ describe Gitlab::SlashCommands::IssueSearch do ...@@ -5,7 +5,7 @@ describe Gitlab::SlashCommands::IssueSearch do
let!(:issue) { create(:issue, project: project, title: 'find me') } let!(:issue) { create(:issue, project: project, title: 'find me') }
let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') } let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { issue.author } let(:user) { create(:user) }
let(:regex_match) { described_class.match("issue search find") } let(:regex_match) { described_class.match("issue search find") }
subject do subject do
......
...@@ -88,5 +88,19 @@ describe Ci::PipelineSchedulePolicy, :models do ...@@ -88,5 +88,19 @@ describe Ci::PipelineSchedulePolicy, :models do
expect(policy).to be_allowed :admin_pipeline_schedule expect(policy).to be_allowed :admin_pipeline_schedule
end end
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
end end
...@@ -92,7 +92,7 @@ describe ProjectPolicy do ...@@ -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 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) project = create(:project, :private)
issue = create(:issue, project: project) issue = create(:issue, project: project, author: create(:user))
user = issue.author user = issue.author
expect(project.team.member?(issue.author)).to be false expect(project.team.member?(issue.author)).to be false
......
...@@ -4,7 +4,7 @@ describe Issues::CloseService do ...@@ -4,7 +4,7 @@ describe Issues::CloseService do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:guest) { 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(:project) { issue.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
......
...@@ -13,7 +13,8 @@ describe Issues::UpdateService, :mailer do ...@@ -13,7 +13,8 @@ describe Issues::UpdateService, :mailer do
create(:issue, title: 'Old title', create(:issue, title: 'Old title',
description: "for #{user2.to_reference}", description: "for #{user2.to_reference}",
assignee_ids: [user3.id], assignee_ids: [user3.id],
project: project) project: project,
author: create(:user))
end end
before do before do
......
...@@ -4,7 +4,7 @@ describe MergeRequests::CloseService do ...@@ -4,7 +4,7 @@ describe MergeRequests::CloseService do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:guest) { 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(:project) { merge_request.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) } let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
......
...@@ -7,7 +7,8 @@ describe MergeRequests::FfMergeService do ...@@ -7,7 +7,8 @@ describe MergeRequests::FfMergeService do
create(:merge_request, create(:merge_request,
source_branch: 'flatten-dir', source_branch: 'flatten-dir',
target_branch: 'improve/awesome', target_branch: 'improve/awesome',
assignee: user2) assignee: user2,
author: create(:user))
end end
let(:project) { merge_request.project } let(:project) { merge_request.project }
......
...@@ -4,7 +4,7 @@ describe MergeRequests::ReopenService do ...@@ -4,7 +4,7 @@ describe MergeRequests::ReopenService do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:guest) { 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 } let(:project) { merge_request.project }
before do before do
......
...@@ -12,7 +12,8 @@ describe MergeRequests::UpdateService, :mailer do ...@@ -12,7 +12,8 @@ describe MergeRequests::UpdateService, :mailer do
create(:merge_request, :simple, title: 'Old title', create(:merge_request, :simple, title: 'Old title',
description: "FYI #{user2.to_reference}", description: "FYI #{user2.to_reference}",
assignee_id: user3.id, assignee_id: user3.id,
source_project: project) source_project: project,
author: create(:user))
end end
before do before do
......
...@@ -458,7 +458,7 @@ describe NotificationService, :mailer do ...@@ -458,7 +458,7 @@ describe NotificationService, :mailer do
context "merge request diff note" do context "merge request diff note" do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:user) { create(:user) } 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) } let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
before do before do
...@@ -469,11 +469,13 @@ describe NotificationService, :mailer do ...@@ -469,11 +469,13 @@ describe NotificationService, :mailer do
describe '#new_note' do describe '#new_note' do
it "records sent notifications" 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 expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(3).times.and_call_original
notification.new_note(note) 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) expect(SentNotification.last.in_reply_to_discussion_id).to eq(note.discussion_id)
end end
end end
......
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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment