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:
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
/* 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');
});
}
/**
......
/* 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) {
......
......@@ -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;
});
}
/**
......
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.')));
}
}
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) {
......
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.')));
}
......@@ -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) {
......
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,
},
});
},
});
}
};
<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 @@
.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,
......
......@@ -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
......@@ -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)
......
......@@ -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)
......
---
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
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.
......
......@@ -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
......
......@@ -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)
......
......@@ -6,6 +6,7 @@ module Gitlab
CommandError = Class.new(StandardError)
CommitError = Class.new(StandardError)
OSError = Class.new(StandardError)
class << self
include Gitlab::EncodingHelper
......
......@@ -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")
......
......@@ -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
#!/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
......
......@@ -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
......
......@@ -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)
......
FactoryBot.define do
factory :event do
project
author factory: :user
author(factory: :user) { project.creator }
action Event::JOINED
trait(:created) { action Event::CREATED }
......
FactoryBot.define do
factory :issue do
title { generate(:title) }
author
project
author { project.creator }
trait :confidential do
confidential true
......
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
......
......@@ -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]
......
......@@ -3,7 +3,7 @@ FactoryBot.define do
skip_create
project
user factory: :user
user { project.creator }
initialize_with { new(project, user) }
end
end
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
......
......@@ -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
......
FactoryBot.define do
factory :subscription do
user
project
user { project.creator }
subscribable factory: :issue
end
end
......@@ -3,7 +3,7 @@
FactoryBot.define do
factory :timelog do
time_spent 3600
user
issue
user { issue.project.creator }
end
end
FactoryBot.define do
factory :todo do
project
author
user
author { project.creator }
user { project.creator }
target factory: :issue
action { Todo::ASSIGNED }
......
......@@ -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
......
......@@ -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
......
......@@ -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" }
......
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 */
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();
});
});
});
});
/* 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();
});
});
});
});
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
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, '')
......
......@@ -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
......
......@@ -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
......@@ -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
......
......@@ -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) }
......
......@@ -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
......
......@@ -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) }
......
......@@ -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 }
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
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