Commit 3569294e authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents 342f33be e9b8fb43
...@@ -75,7 +75,7 @@ stages: ...@@ -75,7 +75,7 @@ stages:
.use-mysql: &use-mysql .use-mysql: &use-mysql
services: services:
- mysql:latest - mysql:5.7
- redis:alpine - redis:alpine
.rails5-variables: &rails5-variables .rails5-variables: &rails5-variables
......
...@@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective: ...@@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective:
Lint/NestedPercentLiteral: Lint/NestedPercentLiteral:
Exclude: Exclude:
- 'lib/gitlab/git/repository.rb' - 'lib/gitlab/git/repository.rb'
- 'spec/support/email_format_shared_examples.rb' - 'spec/support/shared_examples/email_format_shared_examples.rb'
# Offense count: 1 # Offense count: 1
Lint/ReturnInVoidContext: Lint/ReturnInVoidContext:
...@@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase: ...@@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase:
- 'spec/lib/gitlab/diff/parser_spec.rb' - 'spec/lib/gitlab/diff/parser_spec.rb'
- 'spec/lib/json_web_token/rsa_token_spec.rb' - 'spec/lib/json_web_token/rsa_token_spec.rb'
- 'spec/models/commit_spec.rb' - 'spec/models/commit_spec.rb'
- 'spec/support/repo_helpers.rb' - 'spec/support/helpers/repo_helpers.rb'
- 'spec/support/seed_repo.rb' - 'spec/support/helpers/seed_repo.rb'
# Offense count: 112 # Offense count: 112
# Configuration parameters: Blacklist. # Configuration parameters: Blacklist.
...@@ -496,7 +496,7 @@ Style/EmptyLiteral: ...@@ -496,7 +496,7 @@ Style/EmptyLiteral:
- 'spec/lib/gitlab/request_context_spec.rb' - 'spec/lib/gitlab/request_context_spec.rb'
- 'spec/lib/gitlab/workhorse_spec.rb' - 'spec/lib/gitlab/workhorse_spec.rb'
- 'spec/requests/api/jobs_spec.rb' - 'spec/requests/api/jobs_spec.rb'
- 'spec/support/chat_slash_commands_shared_examples.rb' - 'spec/support/shared_examples/chat_slash_commands_shared_examples.rb'
# Offense count: 102 # Offense count: 102
# Cop supports --auto-correct. # Cop supports --auto-correct.
......
...@@ -26,7 +26,9 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ ...@@ -26,7 +26,9 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc) - [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc) - [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc) - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#priority-labels-deliverable-stretch-next-patch-release) - [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc)
- [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests) - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design-ui-elements) - [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker) - [Issue tracker](#issue-tracker)
...@@ -127,6 +129,8 @@ Most issues will have labels for at least one of the following: ...@@ -127,6 +129,8 @@ Most issues will have labels for at least one of the following:
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc. - Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc. - Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release" - Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4
- Severity: ~S1, ~S2, ~S3, ~S4
All labels, their meaning and priority are defined on the All labels, their meaning and priority are defined on the
[labels page][labels-page]. [labels page][labels-page].
...@@ -210,7 +214,7 @@ This label documents the planned timeline & urgency which is used to measure aga ...@@ -210,7 +214,7 @@ This label documents the planned timeline & urgency which is used to measure aga
| Label | Meaning | Estimate time to fix | Guidance | | Label | Meaning | Estimate time to fix | Guidance |
|-------|-----------------|------------------------------------------------------------------|----------| |-------|-----------------|------------------------------------------------------------------|----------|
| ~P1 | Urgent Priority | The current release | | | ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com | |
| ~P2 | High Priority | The next release | | | ~P2 | High Priority | The next release | |
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | | | ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented | | ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
......
...@@ -178,7 +178,7 @@ GEM ...@@ -178,7 +178,7 @@ GEM
docile (1.1.5) docile (1.1.5)
domain_name (0.5.20170404) domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.3.1) doorkeeper (4.3.2)
railties (>= 4.2) railties (>= 4.2)
doorkeeper-openid_connect (1.3.0) doorkeeper-openid_connect (1.3.0)
doorkeeper (~> 4.3) doorkeeper (~> 4.3)
......
...@@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown { ...@@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown {
if (data.can_create_branch) { if (data.can_create_branch) {
this.available(); this.available();
this.enable(); this.enable();
this.updateBranchName(data.suggested_branch_name);
if (!this.droplabInitialized) { if (!this.droplabInitialized) {
this.droplabInitialized = true; this.droplabInitialized = true;
this.initDroplab(); this.initDroplab();
this.bindEvents(); this.bindEvents();
} }
} else if (data.has_related_branch) { } else {
this.hide(); this.hide();
} }
}) })
.catch(() => { .catch(() => {
this.unavailable(); this.unavailable();
this.disable(); this.disable();
Flash('Failed to check if a new branch can be created.'); Flash(__('Failed to check related branches.'));
}); });
} }
...@@ -409,13 +410,16 @@ export default class CreateMergeRequestDropdown { ...@@ -409,13 +410,16 @@ export default class CreateMergeRequestDropdown {
this.unavailableButton.classList.remove('hide'); this.unavailableButton.classList.remove('hide');
} }
updateBranchName(suggestedBranchName) {
this.branchInput.value = suggestedBranchName;
this.updateCreatePaths('branch', suggestedBranchName);
}
updateInputState(target, ref, result) { updateInputState(target, ref, result) {
// target - 'branch' or 'ref' - which the input field we are searching a ref for. // target - 'branch' or 'ref' - which the input field we are searching a ref for.
// ref - string - what a user typed. // ref - string - what a user typed.
// result - string - what has been found on backend. // result - string - what has been found on backend.
const pathReplacement = `$1${ref}`;
// If a found branch equals exact the same text a user typed, // If a found branch equals exact the same text a user typed,
// that means a new branch cannot be created as it already exists. // that means a new branch cannot be created as it already exists.
if (ref === result) { if (ref === result) {
...@@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown { ...@@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown {
this.refIsValid = true; this.refIsValid = true;
this.refInput.dataset.value = ref; this.refInput.dataset.value = ref;
this.showAvailableMessage('ref'); this.showAvailableMessage('ref');
this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath, this.updateCreatePaths(target, ref);
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
pathReplacement);
} }
} else if (target === 'branch') { } else if (target === 'branch') {
this.branchIsValid = true; this.branchIsValid = true;
this.showAvailableMessage('branch'); this.showAvailableMessage('branch');
this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath, this.updateCreatePaths(target, ref);
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
pathReplacement);
} else { } else {
this.refIsValid = false; this.refIsValid = false;
this.refInput.dataset.value = ref; this.refInput.dataset.value = ref;
...@@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown { ...@@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown {
this.disableCreateAction(); this.disableCreateAction();
} }
} }
// target - 'branch' or 'ref'
// ref - string - the new value to use as branch or ref
updateCreatePaths(target, ref) {
const pathReplacement = `$1${ref}`;
this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath,
pathReplacement);
}
} }
...@@ -32,26 +32,38 @@ export default { ...@@ -32,26 +32,38 @@ export default {
required: true, required: true,
}, },
buttonDisabled: { requestFinishedFor: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
data() {
return {
isDisabled: false,
linkRequested: '',
};
},
computed: { computed: {
cssClass() { cssClass() {
const actionIconDash = dasherize(this.actionIcon); const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`; return `${actionIconDash} js-icon-${actionIconDash}`;
}, },
isDisabled() { },
return this.buttonDisabled === this.link; watch: {
requestFinishedFor() {
if (this.requestFinishedFor === this.linkRequested) {
this.isDisabled = false;
}
}, },
}, },
methods: { methods: {
onClickAction() { onClickAction() {
$(this.$el).tooltip('hide'); $(this.$el).tooltip('hide');
eventHub.$emit('graphAction', this.link); eventHub.$emit('graphAction', this.link);
this.linkRequested = this.link;
this.isDisabled = true;
}, },
}, },
}; };
...@@ -62,7 +74,8 @@ export default { ...@@ -62,7 +74,8 @@ export default {
@click="onClickAction" @click="onClickAction"
v-tooltip v-tooltip
:title="tooltipText" :title="tooltipText"
class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper" class="js-ci-action btn btn-blank
btn-transparent ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass" :class="cssClass"
data-container="body" data-container="body"
:disabled="isDisabled" :disabled="isDisabled"
......
<script>
import icon from '../../../vue_shared/components/icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead.
*/
export default {
components: {
icon,
},
directives: {
tooltip,
},
props: {
tooltipText: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
actionMethod: {
type: String,
required: true,
},
actionIcon: {
type: String,
required: true,
},
},
};
</script>
<template>
<a
v-tooltip
:data-method="actionMethod"
:title="tooltipText"
:href="link"
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-container="body"
aria-label="Job's action"
>
<icon :name="actionIcon" />
</a>
</template>
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import jobNameComponent from './job_name_component.vue'; import JobNameComponent from './job_name_component.vue';
import jobComponent from './job_component.vue'; import JobComponent from './job_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
/** /**
* Renders the dropdown for the pipeline graph. * Renders the dropdown for the pipeline graph.
* *
* The following object should be provided as `job`: * The following object should be provided as `job`:
...@@ -27,14 +27,14 @@ ...@@ -27,14 +27,14 @@
* } * }
* } * }
*/ */
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
jobComponent, JobComponent,
jobNameComponent, JobNameComponent,
}, },
props: { props: {
...@@ -42,6 +42,11 @@ ...@@ -42,6 +42,11 @@
type: Object, type: Object,
required: true, required: true,
}, },
requestFinishedFor: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
...@@ -56,22 +61,23 @@ ...@@ -56,22 +61,23 @@
methods: { methods: {
/** /**
* When the user right clicks or cmd/ctrl + click in the job name * When the user right clicks or cmd/ctrl + click in the job name or the action icon
* the dropdown should not be closed and the link should open in another tab, * the dropdown should not be closed so we stop propagation
* so we stop propagation of the click event inside the dropdown. * of the click event inside the dropdown.
* *
* Since this component is rendered multiple times per page we need to guarantee we only * Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component. * target the click event of this component.
*/ */
stopDropdownClickPropagation() { stopDropdownClickPropagation() {
$(this.$el $(
.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) '.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
.on('click', (e) => { this.$el,
).on('click', e => {
e.stopPropagation(); e.stopPropagation();
}); });
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="ci-job-dropdown-container"> <div class="ci-job-dropdown-container">
...@@ -101,8 +107,8 @@ ...@@ -101,8 +107,8 @@
:key="i"> :key="i">
<job-component <job-component
:job="item" :job="item"
:is-dropdown="true"
css-class-job-name="mini-pipeline-graph-dropdown-item" css-class-job-name="mini-pipeline-graph-dropdown-item"
:request-finished-for="requestFinishedFor"
/> />
</li> </li>
</ul> </ul>
......
...@@ -7,7 +7,6 @@ export default { ...@@ -7,7 +7,6 @@ export default {
StageColumnComponent, StageColumnComponent,
LoadingIcon, LoadingIcon,
}, },
props: { props: {
isLoading: { isLoading: {
type: Boolean, type: Boolean,
...@@ -17,10 +16,10 @@ export default { ...@@ -17,10 +16,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
actionDisabled: { requestFinishedFor: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
...@@ -75,7 +74,7 @@ export default { ...@@ -75,7 +74,7 @@ export default {
:key="stage.name" :key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)" :stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)" :is-first-column="isFirstColumn(index)"
:action-disabled="actionDisabled" :request-finished-for="requestFinishedFor"
/> />
</ul> </ul>
</div> </div>
......
<script> <script>
import ActionComponent from './action_component.vue'; import ActionComponent from './action_component.vue';
import DropdownActionComponent from './dropdown_action_component.vue';
import JobNameComponent from './job_name_component.vue'; import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
...@@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip'; ...@@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
export default { export default {
components: { components: {
ActionComponent, ActionComponent,
DropdownActionComponent,
JobNameComponent, JobNameComponent,
}, },
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -44,26 +41,17 @@ export default { ...@@ -44,26 +41,17 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
cssClassJobName: { cssClassJobName: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
requestFinishedFor: {
isDropdown: {
type: Boolean,
required: false,
default: false,
},
actionDisabled: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
computed: { computed: {
status() { status() {
return this.job && this.job.status ? this.job.status : {}; return this.job && this.job.status ? this.job.status : {};
...@@ -134,19 +122,11 @@ export default { ...@@ -134,19 +122,11 @@ export default {
</div> </div>
<action-component <action-component
v-if="hasAction && !isDropdown" v-if="hasAction"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
:button-disabled="actionDisabled"
/>
<dropdown-action-component
v-if="hasAction && isDropdown"
:tooltip-text="status.action.title" :tooltip-text="status.action.title"
:link="status.action.path" :link="status.action.path"
:action-icon="status.action.icon" :action-icon="status.action.icon"
:action-method="status.action.method" :request-finished-for="requestFinishedFor"
/> />
</div> </div>
</template> </template>
...@@ -29,10 +29,11 @@ export default { ...@@ -29,10 +29,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
actionDisabled: {
requestFinishedFor: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
...@@ -74,12 +75,12 @@ export default { ...@@ -74,12 +75,12 @@ export default {
v-if="job.size === 1" v-if="job.size === 1"
:job="job" :job="job"
css-class-job-name="build-content" css-class-job-name="build-content"
:action-disabled="actionDisabled"
/> />
<dropdown-job-component <dropdown-job-component
v-if="job.size > 1" v-if="job.size > 1"
:job="job" :job="job"
:request-finished-for="requestFinishedFor"
/> />
</li> </li>
......
...@@ -25,7 +25,7 @@ export default () => { ...@@ -25,7 +25,7 @@ export default () => {
data() { data() {
return { return {
mediator, mediator,
actionDisabled: null, requestFinishedFor: null,
}; };
}, },
created() { created() {
...@@ -36,15 +36,17 @@ export default () => { ...@@ -36,15 +36,17 @@ export default () => {
}, },
methods: { methods: {
postAction(action) { postAction(action) {
this.actionDisabled = action; // Click was made, reset this variable
this.requestFinishedFor = null;
this.mediator.service.postAction(action) this.mediator.service
.postAction(action)
.then(() => { .then(() => {
this.mediator.refreshPipeline(); this.mediator.refreshPipeline();
this.actionDisabled = null; this.requestFinishedFor = action;
}) })
.catch(() => { .catch(() => {
this.actionDisabled = null; this.requestFinishedFor = action;
Flash(__('An error occurred while making the request.')); Flash(__('An error occurred while making the request.'));
}); });
}, },
...@@ -54,7 +56,7 @@ export default () => { ...@@ -54,7 +56,7 @@ export default () => {
props: { props: {
isLoading: this.mediator.state.isLoading, isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline, pipeline: this.mediator.store.state.pipeline,
actionDisabled: this.actionDisabled, requestFinishedFor: this.requestFinishedFor,
}, },
}); });
}, },
...@@ -79,7 +81,8 @@ export default () => { ...@@ -79,7 +81,8 @@ export default () => {
}, },
methods: { methods: {
postAction(action) { postAction(action) {
this.mediator.service.postAction(action.path) this.mediator.service
.postAction(action.path)
.then(() => this.mediator.refreshPipeline()) .then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.'))); .catch(() => Flash(__('An error occurred while making the request.')));
}, },
......
<script> <script>
import ciIcon from './ci_icon.vue'; import CiIcon from './ci_icon.vue';
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
/** /**
* Renders CI Badge link with CI icon and status text based on * Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used. * API response shared between all places where it is used.
* *
...@@ -22,9 +22,9 @@ ...@@ -22,9 +22,9 @@
* - MR widget * - MR widget
*/ */
export default { export default {
components: { components: {
ciIcon, CiIcon,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
return className ? `ci-status ci-${className}` : 'ci-status'; return className ? `ci-status ci-${className}` : 'ci-status';
}, },
}, },
}; };
</script> </script>
<template> <template>
<a <a
......
<script> <script>
import icon from '../../vue_shared/components/icon.vue'; import Icon from '../../vue_shared/components/icon.vue';
/** /**
* Renders CI icon based on API response shared between all places where it is used. * Renders CI icon based on API response shared between all places where it is used.
* *
* Receives status object containing: * Receives status object containing:
...@@ -22,9 +22,9 @@ ...@@ -22,9 +22,9 @@
* - Jobs show view header * - Jobs show view header
* - Jobs show view sidebar * - Jobs show view sidebar
*/ */
export default { export default {
components: { components: {
icon, Icon,
}, },
props: { props: {
status: { status: {
...@@ -32,14 +32,13 @@ ...@@ -32,14 +32,13 @@
required: true, required: true,
}, },
}, },
computed: { computed: {
cssClass() { cssClass() {
const status = this.status.group; const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`; return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
}, },
}, },
}; };
</script> </script>
<template> <template>
<span :class="cssClass"> <span :class="cssClass">
......
<script> <script>
/** /**
* Falls back to the code used in `copy_to_clipboard.js` * Falls back to the code used in `copy_to_clipboard.js`
*
* Renders a button with a clipboard icon that copies the content of `data-clipboard-text`
* when clicked.
*
* @example
* <clipboard-button
* title="Copy to clipbard"
* text="Content to be copied"
* css-class="btn-transparent"
* />
*/ */
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
export default { export default {
name: 'ClipboardButton', name: 'ClipboardButton',
directives: { directives: {
tooltip, tooltip,
...@@ -34,7 +44,7 @@ ...@@ -34,7 +44,7 @@
default: 'btn-default', default: 'btn-default',
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import commitIconSvg from 'icons/_icon_commit.svg'; import UserAvatarLink from './user_avatar/user_avatar_link.vue';
import userAvatarLink from './user_avatar/user_avatar_link.vue'; import tooltip from '../directives/tooltip';
import tooltip from '../directives/tooltip'; import Icon from '../../vue_shared/components/icon.vue';
import icon from '../../vue_shared/components/icon.vue';
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
userAvatarLink, UserAvatarLink,
icon, Icon,
}, },
props: { props: {
/** /**
...@@ -94,10 +93,7 @@ ...@@ -94,10 +93,7 @@
* @returns {Boolean} * @returns {Boolean}
*/ */
hasAuthor() { hasAuthor() {
return this.author && return this.author && this.author.avatar_url && this.author.path && this.author.username;
this.author.avatar_url &&
this.author.path &&
this.author.username;
}, },
/** /**
* If information about the author is provided will return a string * If information about the author is provided will return a string
...@@ -106,14 +102,10 @@ ...@@ -106,14 +102,10 @@
* @returns {String} * @returns {String}
*/ */
userImageAltDescription() { userImageAltDescription() {
return this.author && return this.author && this.author.username ? `${this.author.username}'s avatar` : null;
this.author.username ? `${this.author.username}'s avatar` : null;
}, },
}, },
created() { };
this.commitIconSvg = commitIconSvg;
},
};
</script> </script>
<template> <template>
<div class="branch-commit"> <div class="branch-commit">
...@@ -141,11 +133,10 @@ ...@@ -141,11 +133,10 @@
{{ commitRef.name }} {{ commitRef.name }}
</a> </a>
</template> </template>
<div <icon
v-html="commitIconSvg" name="commit"
class="commit-icon js-commit-icon" class="commit-icon js-commit-icon"
> />
</div>
<a <a
class="commit-sha" class="commit-sha"
......
<script> <script>
import { __ } from '~/locale'; import { __ } from '~/locale';
/** /**
* Port of detail_behavior expand button. * Port of detail_behavior expand button.
* *
* @example * @example
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
* </template> * </template>
* </expand-button> * </expand-button>
*/ */
export default { export default {
name: 'ExpandButton', name: 'ExpandButton',
data() { data() {
return { return {
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
this.isCollapsed = !this.isCollapsed; this.isCollapsed = !this.isCollapsed;
}, },
}, },
}; };
</script> </script>
<template> <template>
<span> <span>
......
<script> <script>
import ciIconBadge from './ci_badge_link.vue'; import CiIconBadge from './ci_badge_link.vue';
import loadingIcon from './loading_icon.vue'; import LoadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue'; import TimeagoTooltip from './time_ago_tooltip.vue';
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
import userAvatarImage from './user_avatar/user_avatar_image.vue'; import UserAvatarImage from './user_avatar/user_avatar_image.vue';
/** /**
* Renders header component for job and pipeline page based on UI mockups * Renders header component for job and pipeline page based on UI mockups
* *
* Used in: * Used in:
* - job show page * - job show page
* - pipeline show page * - pipeline show page
*/ */
export default { export default {
components: { components: {
ciIconBadge, CiIconBadge,
loadingIcon, LoadingIcon,
timeagoTooltip, TimeagoTooltip,
userAvatarImage, UserAvatarImage,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
this.$emit('actionClicked', action); this.$emit('actionClicked', action);
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
/* This is a re-usable vue component for rendering a svg sprite
/* This is a re-usable vue component for rendering a svg sprite
icon icon
Sample configuration: Sample configuration:
...@@ -11,11 +10,11 @@ ...@@ -11,11 +10,11 @@
css-classes="top" css-classes="top"
/> />
*/ */
// only allow classes in images.scss e.g. s12 // only allow classes in images.scss e.g. s12
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
export default { export default {
props: { props: {
name: { name: {
type: String, type: String,
...@@ -70,7 +69,7 @@ ...@@ -70,7 +69,7 @@
return this.size ? `s${this.size}` : ''; return this.size ? `s${this.size}` : '';
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -79,7 +78,8 @@ ...@@ -79,7 +78,8 @@
:width="width" :width="width"
:height="height" :height="height"
:x="x" :x="x"
:y="y"> :y="y"
>
<use v-bind="{ 'xlink:href':spriteHref }" /> <use v-bind="{ 'xlink:href':spriteHref }" />
</svg> </svg>
</template> </template>
<script> <script>
import $ from 'jquery';
import { __ } from '~/locale'; import { __ } from '~/locale';
import LabelsSelect from '~/labels_select'; import LabelsSelect from '~/labels_select';
import LoadingIcon from '../../loading_icon.vue'; import LoadingIcon from '../../loading_icon.vue';
...@@ -98,11 +99,18 @@ export default { ...@@ -98,11 +99,18 @@ export default {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, { this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick, handleClick: this.handleClick,
}); });
$(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
}, },
methods: { methods: {
handleClick(label) { handleClick(label) {
this.$emit('onLabelClick', label); this.$emit('onLabelClick', label);
}, },
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
handleDropdownHidden() {
this.$emit('onDropdownClose');
},
}, },
}; };
</script> </script>
...@@ -112,6 +120,7 @@ export default { ...@@ -112,6 +120,7 @@ export default {
<dropdown-value-collapsed <dropdown-value-collapsed
v-if="showCreate" v-if="showCreate"
:labels="context.labels" :labels="context.labels"
@onValueClick="handleCollapsedValueClick"
/> />
<dropdown-title <dropdown-title
:can-edit="canEdit" :can-edit="canEdit"
...@@ -133,7 +142,10 @@ export default { ...@@ -133,7 +142,10 @@ export default {
:name="hiddenInputName" :name="hiddenInputName"
:label="label" :label="label"
/> />
<div class="dropdown"> <div
class="dropdown"
ref="dropdown"
>
<dropdown-button <dropdown-button
:ability-name="abilityName" :ability-name="abilityName"
:field-name="hiddenInputName" :field-name="hiddenInputName"
......
...@@ -26,6 +26,11 @@ export default { ...@@ -26,6 +26,11 @@ export default {
return labelsString; return labelsString;
}, },
}, },
methods: {
handleClick() {
this.$emit('onValueClick');
},
},
}; };
</script> </script>
...@@ -36,6 +41,7 @@ export default { ...@@ -36,6 +41,7 @@ export default {
data-placement="left" data-placement="left"
data-container="body" data-container="body"
:title="labelsList" :title="labelsList"
@click="handleClick"
> >
<i <i
aria-hidden="true" aria-hidden="true"
......
...@@ -468,6 +468,14 @@ ...@@ -468,6 +468,14 @@
margin-bottom: 10px; margin-bottom: 10px;
white-space: normal; white-space: normal;
.ci-job-dropdown-container {
// override dropdown.scss
.dropdown-menu li button {
padding: 0;
text-align: center;
}
}
// ensure .build-content has hover style when action-icon is hovered // ensure .build-content has hover style when action-icon is hovered
.ci-job-dropdown-container:hover .build-content { .ci-job-dropdown-container:hover .build-content {
@extend .build-content:hover; @extend .build-content:hover;
......
...@@ -134,11 +134,11 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -134,11 +134,11 @@ class Projects::IssuesController < Projects::ApplicationController
def can_create_branch def can_create_branch
can_create = current_user && can_create = current_user &&
can?(current_user, :push_code, @project) && can?(current_user, :push_code, @project) &&
@issue.can_be_worked_on?(current_user) @issue.can_be_worked_on?
respond_to do |format| respond_to do |format|
format.json do format.json do
render json: { can_create_branch: can_create, has_related_branch: @issue.has_related_branch? } render json: { can_create_branch: can_create, suggested_branch_name: @issue.suggested_branch_name }
end end
end end
end end
...@@ -177,7 +177,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -177,7 +177,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def authorize_create_merge_request! def authorize_create_merge_request!
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?
end end
def render_issue_json def render_issue_json
......
...@@ -78,8 +78,6 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -78,8 +78,6 @@ class Projects::JobsController < Projects::ApplicationController
result.merge!(trace.to_h) result.merge!(trace.to_h)
end end
result[:html] = result[:html].presence || 'No job log'
render json: result render json: result
end end
end end
......
...@@ -32,6 +32,7 @@ class UsersFinder ...@@ -32,6 +32,7 @@ class UsersFinder
users = by_active(users) users = by_active(users)
users = by_external_identity(users) users = by_external_identity(users)
users = by_external(users) users = by_external(users)
users = by_2fa(users)
users = by_created_at(users) users = by_created_at(users)
users = by_custom_attributes(users) users = by_custom_attributes(users)
...@@ -76,4 +77,15 @@ class UsersFinder ...@@ -76,4 +77,15 @@ class UsersFinder
users.external users.external
end end
def by_2fa(users)
case params[:two_factor]
when 'enabled'
users.with_two_factor
when 'disabled'
users.without_two_factor
else
users
end
end
end end
...@@ -27,8 +27,9 @@ module AtomicInternalId ...@@ -27,8 +27,9 @@ module AtomicInternalId
module ClassMethods module ClassMethods
def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName
before_validation(on: :create) do before_validation(on: :create) do
if read_attribute(column).blank? scope_value = association(scope).reader
scope_attrs = { scope => association(scope).reader } if read_attribute(column).blank? && scope_value
scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
usage = self.class.table_name.to_sym usage = self.class.table_name.to_sym
new_iid = InternalId.generate_next(self, scope_attrs, usage, init) new_iid = InternalId.generate_next(self, scope_attrs, usage, init)
......
...@@ -31,12 +31,13 @@ module Avatarable ...@@ -31,12 +31,13 @@ module Avatarable
asset_host = ActionController::Base.asset_host asset_host = ActionController::Base.asset_host
use_asset_host = asset_host.present? use_asset_host = asset_host.present?
use_authentication = respond_to?(:public?) && !public?
# Avatars for private and internal groups and projects require authentication to be viewed, # Avatars for private and internal groups and projects require authentication to be viewed,
# which means they can only be served by Rails, on the regular GitLab host. # which means they can only be served by Rails, on the regular GitLab host.
# If an asset host is configured, we need to return the fully qualified URL # If an asset host is configured, we need to return the fully qualified URL
# instead of only the avatar path, so that Rails doesn't prefix it with the asset host. # instead of only the avatar path, so that Rails doesn't prefix it with the asset host.
if use_asset_host && respond_to?(:public?) && !public? if use_asset_host && use_authentication
use_asset_host = false use_asset_host = false
only_path = false only_path = false
end end
...@@ -49,6 +50,6 @@ module Avatarable ...@@ -49,6 +50,6 @@ module Avatarable
url_base << gitlab_config.relative_url_root url_base << gitlab_config.relative_url_root
end end
url_base + avatar.url url_base + avatar.local_url
end end
end end
module NonatomicInternalId
extend ActiveSupport::Concern
included do
validate :set_iid, on: :create
validates :iid, presence: true, numericality: true
end
def set_iid
if iid.blank?
parent = project || group
records = parent.public_send(self.class.name.tableize) # rubocop:disable GitlabSecurity/PublicSend
max_iid = records.maximum(:iid)
self.iid = max_iid.to_i + 1
end
end
def to_param
iid.to_s
end
end
...@@ -102,7 +102,7 @@ module Routable ...@@ -102,7 +102,7 @@ module Routable
# the route. Caching this per request ensures that even if we have multiple instances, # the route. Caching this per request ensures that even if we have multiple instances,
# we will not have to duplicate work, avoiding N+1 queries in some cases. # we will not have to duplicate work, avoiding N+1 queries in some cases.
def full_path def full_path
return uncached_full_path unless RequestStore.active? return uncached_full_path unless RequestStore.active? && persisted?
RequestStore[full_path_key] ||= uncached_full_path RequestStore[full_path_key] ||= uncached_full_path
end end
...@@ -124,6 +124,11 @@ module Routable ...@@ -124,6 +124,11 @@ module Routable
end end
end end
# Group would override this to check from association
def owned_by?(user)
owner == user
end
private private
def set_path_errors def set_path_errors
......
# Uniquify
#
# Return a version of the given 'base' string that is unique
# by appending a counter to it. Uniqueness is determined by
# repeated calls to the passed block.
#
# You can pass an initial value for the counter, if not given
# counting starts from 1.
#
# If `base` is a function/proc, we expect that calling it with a
# candidate counter returns a string to test/return.
class Uniquify class Uniquify
# Return a version of the given 'base' string that is unique def initialize(counter = nil)
# by appending a counter to it. Uniqueness is determined by @counter = counter
# repeated calls to the passed block. end
#
# If `base` is a function/proc, we expect that calling it with a
# candidate counter returns a string to test/return.
def string(base) def string(base)
@base = base @base = base
@counter = nil
increment_counter! while yield(base_string) increment_counter! while yield(base_string)
base_string base_string
......
class Deployment < ActiveRecord::Base class Deployment < ActiveRecord::Base
include NonatomicInternalId include AtomicInternalId
belongs_to :project, required: true belongs_to :project, required: true
belongs_to :environment, required: true belongs_to :environment, required: true
belongs_to :user belongs_to :user
belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.deployments&.maximum(:iid) }
validates :sha, presence: true validates :sha, presence: true
validates :ref, presence: true validates :ref, presence: true
......
...@@ -125,6 +125,10 @@ class Group < Namespace ...@@ -125,6 +125,10 @@ class Group < Namespace
self[:lfs_enabled] self[:lfs_enabled]
end end
def owned_by?(user)
owners.include?(user)
end
def add_users(users, access_level, current_user: nil, expires_at: nil) def add_users(users, access_level, current_user: nil, expires_at: nil)
GroupMember.add_users( GroupMember.add_users(
self, self,
......
...@@ -12,8 +12,9 @@ ...@@ -12,8 +12,9 @@
# * (Optionally) add columns to `internal_ids` if needed for scope. # * (Optionally) add columns to `internal_ids` if needed for scope.
class InternalId < ActiveRecord::Base class InternalId < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :namespace
enum usage: { issues: 0 } enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4 }
validates :usage, presence: true validates :usage, presence: true
......
...@@ -194,6 +194,15 @@ class Issue < ActiveRecord::Base ...@@ -194,6 +194,15 @@ class Issue < ActiveRecord::Base
branches_with_iid - branches_with_merge_request branches_with_iid - branches_with_merge_request
end end
def suggested_branch_name
return to_branch_name unless project.repository.branch_exists?(to_branch_name)
start_counting_from = 2
Uniquify.new(start_counting_from).string(-> (counter) { "#{to_branch_name}-#{counter}" }) do |suggested_branch_name|
project.repository.branch_exists?(suggested_branch_name)
end
end
# Returns boolean if a related branch exists for the current issue # Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs # ignores merge requests branchs
def has_related_branch? def has_related_branch?
...@@ -248,11 +257,8 @@ class Issue < ActiveRecord::Base ...@@ -248,11 +257,8 @@ class Issue < ActiveRecord::Base
end end
end end
def can_be_worked_on?(current_user) def can_be_worked_on?
!self.closed? && !self.closed? && !self.project.forked?
!self.project.forked? &&
self.related_branches(current_user).empty? &&
self.closed_by_merge_requests(current_user).empty?
end end
# Returns `true` if the current issue can be viewed by either a logged in User # Returns `true` if the current issue can be viewed by either a logged in User
......
class MergeRequest < ActiveRecord::Base class MergeRequest < ActiveRecord::Base
include NonatomicInternalId include AtomicInternalId
include Issuable include Issuable
include Noteable include Noteable
include Referable include Referable
...@@ -18,6 +18,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -18,6 +18,8 @@ class MergeRequest < ActiveRecord::Base
belongs_to :source_project, class_name: "Project" belongs_to :source_project, class_name: "Project"
belongs_to :merge_user, class_name: "User" belongs_to :merge_user, class_name: "User"
has_internal_id :iid, scope: :target_project, init: ->(s) { s&.target_project&.merge_requests&.maximum(:iid) }
has_many :merge_request_diffs has_many :merge_request_diffs
has_one :merge_request_diff, has_one :merge_request_diff,
......
...@@ -8,7 +8,7 @@ class Milestone < ActiveRecord::Base ...@@ -8,7 +8,7 @@ class Milestone < ActiveRecord::Base
Started = MilestoneStruct.new('Started', '#started', -3) Started = MilestoneStruct.new('Started', '#started', -3)
include CacheMarkdownField include CacheMarkdownField
include NonatomicInternalId include AtomicInternalId
include Sortable include Sortable
include Referable include Referable
include StripAttribute include StripAttribute
...@@ -21,6 +21,9 @@ class Milestone < ActiveRecord::Base ...@@ -21,6 +21,9 @@ class Milestone < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :group belongs_to :group
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.milestones&.maximum(:iid) }
has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.milestones&.maximum(:iid) }
has_many :issues has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests has_many :merge_requests
......
require "flowdock-git-hook" require "flowdock-git-hook"
# Flow dock depends on Grit to compute the number of commits between two given
# commits. To make this depend on Gitaly, a monkey patch is applied
module Flowdock
class Git
# pass down a Repository all the way down
def repo
@options[:repo]
end
def config
{}
end
def messages
Git::Builder.new(repo: repo,
ref: @ref,
before: @from,
after: @to,
commit_url: @commit_url,
branch_url: @branch_url,
diff_url: @diff_url,
repo_url: @repo_url,
repo_name: @repo_name,
permanent_refs: @permanent_refs,
tags: tags
).to_hashes
end
class Builder
def commits
@repo.commits_between(@before, @after).map do |commit|
{
url: @opts[:commit_url] ? @opts[:commit_url] % [commit.sha] : nil,
id: commit.sha,
message: commit.message,
author: {
name: commit.author_name,
email: commit.author_email
}
}
end
end
end
end
end
class FlowdockService < Service class FlowdockService < Service
prop_accessor :token prop_accessor :token
validates :token, presence: true, if: :activated? validates :token, presence: true, if: :activated?
...@@ -34,7 +80,7 @@ class FlowdockService < Service ...@@ -34,7 +80,7 @@ class FlowdockService < Service
data[:before], data[:before],
data[:after], data[:after],
token: token, token: token,
repo: project.repository.path_to_repo, repo: project.repository,
repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}", repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}",
commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s", commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s",
diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s" diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s"
......
...@@ -260,7 +260,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -260,7 +260,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout
OpenStruct.new(enabled: auto_devops_enabled?, OpenStruct.new(enabled: auto_devops_enabled?,
label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'), label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'),
link: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) link: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
elsif auto_devops_enabled? elsif auto_devops_enabled?
OpenStruct.new(enabled: true, OpenStruct.new(enabled: true,
label: _('Auto DevOps enabled'), label: _('Auto DevOps enabled'),
......
...@@ -65,6 +65,10 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -65,6 +65,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
!!model !!model
end end
def local_url
File.join('/', self.class.base_dir, dynamic_segment, filename)
end
private private
# Designed to be overridden by child uploaders that have a dynamic path # Designed to be overridden by child uploaders that have a dynamic path
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
.help-block .help-block
Manage repository storage paths. Learn more in the Manage repository storage paths. Learn more in the
= succeed "." do = succeed "." do
= link_to "repository storages documentation", help_page_path("administration/repository_storages") = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths")
.sub-section .sub-section
%h4 Circuit breaker %h4 Circuit breaker
.form-group .form-group
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
%hr %hr
%p %p
- link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings')) - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'))
- link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project)) - link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project))
= s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster } = s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster }
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
- unless @repository.gitlab_ci_yml - unless @repository.gitlab_ci_yml
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do = link_to project_ci_lint_path(@project), class: 'btn btn-default' do
%span CI lint %span CI lint
.content-list.builds-content-list .content-list.builds-content-list
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
%ul %ul
- pipeline.yaml_errors.split(",").each do |error| - pipeline.yaml_errors.split(",").each do |error|
%li= error %li= error
You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} You can also test your .gitlab-ci.yml in the #{link_to "Lint", project_ci_lint_path(@project)}
- if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
.bs-callout.bs-callout-warning .bs-callout.bs-callout-warning
......
.row.prepend-top-default
.col-lg-12
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project)
%fieldset.builds-feature
.form-group
- message = auto_devops_warning_message(@project)
- ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe
- if message
%p.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio
= form.label :enabled_true do
= form.radio_button :enabled, 'true'
%strong= s_('CICD|Enable Auto DevOps')
%br
= s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted }
.radio
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
%strong= s_('CICD|Disable Auto DevOps')
%br
= s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted }
.radio
= form.label :enabled_ do
= form.radio_button :enabled, ''
%strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" }
%br
= s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted }
= form.label :domain, class:"prepend-top-10" do
= _('Domain')
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
.help-block
= s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.')
= f.submit 'Save changes', class: "btn btn-success prepend-top-15"
...@@ -3,44 +3,6 @@ ...@@ -3,44 +3,6 @@
= form_for @project, url: project_settings_ci_cd_path(@project) do |f| = form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project) = form_errors(@project)
%fieldset.builds-feature %fieldset.builds-feature
.form-group
%h5 Auto DevOps (Beta)
%p
Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.
= link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
- message = auto_devops_warning_message(@project)
- if message
%p.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio
= form.label :enabled_true do
= form.radio_button :enabled, 'true'
%strong Enable Auto DevOps
%br
%span.descr
The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
.radio
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
%strong Disable Auto DevOps
%br
%span.descr
An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
.radio
= form.label :enabled_ do
= form.radio_button :enabled, ''
%strong Instance default (#{Gitlab::CurrentSettings.auto_devops_enabled? ? 'enabled' : 'disabled'})
%br
%span.descr
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
%p
You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
%hr
.form-group.append-bottom-default.js-secret-runner-token .form-group.append-bottom-default.js-secret-runner-token
= f.label :runners_token, "Runner token", class: 'label-light' = f.label :runners_token, "Runner token", class: 'label-light'
.form-control.js-secret-value-placeholder .form-control.js-secret-value-placeholder
......
...@@ -12,10 +12,22 @@ ...@@ -12,10 +12,22 @@
%button.btn.js-settings-toggle{ type: 'button' } %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Update your CI/CD configuration, like job timeout or Auto DevOps. Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.
.settings-content .settings-content
= render 'form' = render 'form'
%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= s_('CICD|Auto DevOps (Beta)')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= s_('CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.')
= link_to s_('CICD|Learn more about Auto DevOps'), help_page_path('topics/autodevops/index.md')
.settings-content
= render 'autodevops_form'
%section.settings.no-animate{ class: ('expanded' if expanded) } %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
.banner-buttons .banner-buttons
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout' = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn js-close-callout'
%button.btn-transparent.banner-close.close.js-close-callout{ type: 'button', %button.btn-transparent.banner-close.close.js-close-callout{ type: 'button',
'aria-label' => 'Dismiss Auto DevOps box' } 'aria-label' => 'Dismiss Auto DevOps box' }
......
---
title: Prevent pipeline actions in dropdown to redirct to a new page
merge_request:
author:
type: fixed
---
title: Add an API endpoint to download git repository snapshots
merge_request: 18173
author:
type: added
---
title: Show new branch/mr button even when branch exists
merge_request: 17712
author: Jacopo Beschi @jacopo-beschi
type: added
---
title: Fix discussions API setting created_at for notable in a group or notable in
a project in a group with owners
merge_request: 18464
author:
type: fixed
---
title: Create settings section for autodevops
merge_request: 18321
author:
type: changed
---
title: Fixed wrong avatar URL when the avatar is on object storage.
merge_request: 18092
author:
type: fixed
---
title: Fix project creation for user endpoint when jobs_enabled parameter supplied
merge_request:
author:
type: fixed
---
title: Removes 'No Job log' message from build trace
merge_request: 18523
author:
type: fixed
---
title: Update links to /ci/lint with ones to project ci/lint
merge_request: 18539
author: Takuya Noguchi
type: fixed
---
title: Transition to atomic internal ids for all models.
merge_request: 44259
author:
type: other
---
title: Add missing changelog type to docs
merge_request: 18526
author: "@blackst0ne"
type: other
---
title: Add 2FA filter to users API for admins only
merge_request: 18503
author:
type: changed
---
title: Fix missing namespace for some internal users
merge_request: 18357
author:
type: fixed
---
title: Don't include lfs_file_locks data in export bundle
merge_request: 18495
author:
type: fixed
---
title: Update doorkeeper to 4.3.2 to fix GitLab OAuth authentication
merge_request: 18543
author:
type: fixed
# rubocop:disable GitlabSecurity/PublicSend require_dependency File.expand_path('../../lib/gitlab', __dir__) # Load Gitlab as soon as possible
require_dependency Rails.root.join('lib/gitlab') # Load Gitlab as soon as possible
class Settings < Settingslogic
source ENV.fetch('GITLAB_CONFIG') { "#{Rails.root}/config/gitlab.yml" }
namespace Rails.env
class << self
def gitlab_on_standard_port?
on_standard_port?(gitlab)
end
def host_without_www(url)
host(url).sub('www.', '')
end
def build_gitlab_ci_url
custom_port =
if on_standard_port?(gitlab)
nil
else
":#{gitlab.port}"
end
[
gitlab.protocol,
"://",
gitlab.host,
custom_port,
gitlab.relative_url_root
].join('')
end
def build_pages_url
base_url(pages).join('')
end
def build_gitlab_shell_ssh_path_prefix
user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
if gitlab_shell.ssh_port != 22
"ssh://#{user_host}:#{gitlab_shell.ssh_port}/"
else
if gitlab_shell.ssh_host.include? ':'
"[#{user_host}]:"
else
"#{user_host}:"
end
end
end
def build_base_gitlab_url
base_url(gitlab).join('')
end
def build_gitlab_url
(base_url(gitlab) + [gitlab.relative_url_root]).join('')
end
# check that values in `current` (string or integer) is a contant in `modul`.
def verify_constant_array(modul, current, default)
values = default || []
unless current.nil?
values = []
current.each do |constant|
values.push(verify_constant(modul, constant, nil))
end
values.delete_if { |value| value.nil? }
end
values
end
# check that `current` (string or integer) is a contant in `modul`.
def verify_constant(modul, current, default)
constant = modul.constants.find { |name| modul.const_get(name) == current }
value = constant.nil? ? default : modul.const_get(constant)
if current.is_a? String
value = modul.const_get(current.upcase) rescue default
end
value
end
def absolute(path)
File.expand_path(path, Rails.root)
end
private
def base_url(config)
custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
[
config.protocol,
"://",
config.host,
custom_port
]
end
def on_standard_port?(config)
config.port.to_i == (config.https ? 443 : 80)
end
# Extract the host part of the given +url+.
def host(url)
url = url.downcase
url = "http://#{url}" unless url.start_with?('http')
# Get rid of the path so that we don't even have to encode it
url_without_path = url.sub(%r{(https?://[^/]+)/?.*}, '\1')
URI.parse(url_without_path).host
end
# Runs every minute in a random ten-minute period on Sundays, to balance the
# load on the server receiving these pings. The usage ping is safe to run
# multiple times because of a 24 hour exclusive lock.
def cron_for_usage_ping
hour = rand(24)
minute = rand(6)
"#{minute}0-#{minute}9 #{hour} * * 0"
end
end
end
# Default settings # Default settings
Settings['ldap'] ||= Settingslogic.new({}) Settings['ldap'] ||= Settingslogic.new({})
......
module Gitlab
def self.config
Settings
end
VERSION = File.read(Rails.root.join("VERSION")).strip.freeze
REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp.freeze
end
require './spec/support/sidekiq' require './spec/support/sidekiq'
require './spec/support/test_env' require './spec/support/helpers/test_env'
class Gitlab::Seeder::CycleAnalytics class Gitlab::Seeder::CycleAnalytics
def initialize(project, perf: false) def initialize(project, perf: false)
......
class CreateMissingNamespaceForInternalUsers < ActiveRecord::Migration
DOWNTIME = false
def up
connection.exec_query(users_query.to_sql).rows.each do |id, username|
create_namespace(id, username)
# When testing locally I've noticed that these internal users are missing
# the notification email, for more details visit the below link:
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18357#note_68327560
set_notification_email(id)
end
end
def down
# no-op
end
private
def users
@users ||= Arel::Table.new(:users)
end
def namespaces
@namespaces ||= Arel::Table.new(:namespaces)
end
def users_query
condition = users[:ghost].eq(true)
if column_exists?(:users, :support_bot)
condition = condition.or(users[:support_bot].eq(true))
end
users.join(namespaces, Arel::Nodes::OuterJoin)
.on(namespaces[:type].eq(nil).and(namespaces[:owner_id].eq(users[:id])))
.where(namespaces[:owner_id].eq(nil))
.where(condition)
.project(users[:id], users[:username])
end
def create_namespace(user_id, username)
path = Uniquify.new.string(username) do |str|
query = "SELECT id FROM namespaces WHERE parent_id IS NULL AND path='#{str}' LIMIT 1"
connection.exec_query(query).present?
end
insert_query = "INSERT INTO namespaces(owner_id, path, name) VALUES(#{user_id}, '#{path}', '#{path}')"
namespace_id = connection.insert_sql(insert_query)
create_route(namespace_id)
end
def create_route(namespace_id)
return unless namespace_id
row = connection.exec_query("SELECT id, path FROM namespaces WHERE id=#{namespace_id}").first
id, path = row.values_at('id', 'path')
execute("INSERT INTO routes(source_id, source_type, path, name) VALUES(#{id}, 'Namespace', '#{path}', '#{path}')")
end
def set_notification_email(user_id)
execute "UPDATE users SET notification_email = email WHERE notification_email IS NULL AND id = #{user_id}"
end
end
class AddFurtherScopeColumnsToInternalIdTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
change_column_null :internal_ids, :project_id, true
add_column :internal_ids, :namespace_id, :integer, null: true
end
def down
change_column_null :internal_ids, :project_id, false
remove_column :internal_ids, :namespace_id
end
end
class AddIndexConstraintsToInternalIdTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :internal_ids, [:usage, :namespace_id], unique: true, where: 'namespace_id IS NOT NULL'
replace_index(:internal_ids, [:usage, :project_id], name: 'index_internal_ids_on_usage_and_project_id') do
add_concurrent_index :internal_ids, [:usage, :project_id], unique: true, where: 'project_id IS NOT NULL'
end
add_concurrent_foreign_key :internal_ids, :namespaces, column: :namespace_id, on_delete: :cascade
end
def down
remove_concurrent_index :internal_ids, [:usage, :namespace_id]
replace_index(:internal_ids, [:usage, :project_id], name: 'index_internal_ids_on_usage_and_project_id') do
add_concurrent_index :internal_ids, [:usage, :project_id], unique: true
end
remove_foreign_key :internal_ids, column: :namespace_id
end
private
def replace_index(table, columns, name:)
temporary_name = "#{name}_old"
if index_exists?(table, columns, name: name)
rename_index table, name, temporary_name
end
yield
remove_concurrent_index_by_name table, temporary_name
end
end
...@@ -896,12 +896,14 @@ ActiveRecord::Schema.define(version: 20180418053107) do ...@@ -896,12 +896,14 @@ ActiveRecord::Schema.define(version: 20180418053107) do
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
create_table "internal_ids", id: :bigserial, force: :cascade do |t| create_table "internal_ids", id: :bigserial, force: :cascade do |t|
t.integer "project_id", null: false t.integer "project_id"
t.integer "usage", null: false t.integer "usage", null: false
t.integer "last_value", null: false t.integer "last_value", null: false
t.integer "namespace_id"
end end
add_index "internal_ids", ["usage", "project_id"], name: "index_internal_ids_on_usage_and_project_id", unique: true, using: :btree add_index "internal_ids", ["usage", "namespace_id"], name: "index_internal_ids_on_usage_and_namespace_id", unique: true, where: "(namespace_id IS NOT NULL)", using: :btree
add_index "internal_ids", ["usage", "project_id"], name: "index_internal_ids_on_usage_and_project_id", unique: true, where: "(project_id IS NOT NULL)", using: :btree
create_table "issue_assignees", id: false, force: :cascade do |t| create_table "issue_assignees", id: false, force: :cascade do |t|
t.integer "user_id", null: false t.integer "user_id", null: false
...@@ -2113,6 +2115,7 @@ ActiveRecord::Schema.define(version: 20180418053107) do ...@@ -2113,6 +2115,7 @@ ActiveRecord::Schema.define(version: 20180418053107) do
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "internal_ids", "namespaces", name: "fk_162941d509", on_delete: :cascade
add_foreign_key "internal_ids", "projects", on_delete: :cascade add_foreign_key "internal_ids", "projects", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
......
...@@ -650,6 +650,47 @@ gitlab_rails['redis_sentinels'] = [ ...@@ -650,6 +650,47 @@ gitlab_rails['redis_sentinels'] = [
Omnibus GitLab configures some things behind the curtains to make the sysadmins' Omnibus GitLab configures some things behind the curtains to make the sysadmins'
lives easier. If you want to know what happens underneath keep reading. lives easier. If you want to know what happens underneath keep reading.
### Running multiple Redis clusters
GitLab supports running [separate Redis clusters for different persistent
classes](https://docs.gitlab.com/omnibus/settings/redis.html#running-with-multiple-redis-instances):
cache, queues, and shared_state. To make this work with Sentinel:
1. Set the appropriate variable in `/etc/gitlab/gitlab.rb` for each instance you are using:
```ruby
gitlab_rails['redis_cache_instance'] = REDIS_CACHE_URL
gitlab_rails['redis_queues_instance'] = REDIS_QUEUES_URL
gitlab_rails['redis_shared_state_instance'] = REDIS_SHARED_STATE_URL
```
**Note**: Redis URLs should be in the format: `redis://:PASSWORD@SENTINEL_MASTER_NAME`
1. PASSWORD is the plaintext password for the Redis instance
2. SENTINEL_MASTER_NAME is the Sentinel master name (e.g. `gitlab-redis-cache`)
1. Include an array of hashes with host/port combinations, such as the following:
```ruby
gitlab_rails['redis_cache_sentinels'] = [
{ host: REDIS_CACHE_SENTINEL_HOST, port: PORT1 },
{ host: REDIS_CACHE_SENTINEL_HOST2, port: PORT2 }
]
gitlab_rails['redis_queues_sentinels'] = [
{ host: REDIS_QUEUES_SENTINEL_HOST, port: PORT1 },
{ host: REDIS_QUEUES_SENTINEL_HOST2, port: PORT2 }
]
gitlab_rails['redis_shared_state_sentinels'] = [
{ host: SHARED_STATE_SENTINEL_HOST, port: PORT1 },
{ host: SHARED_STATE_SENTINEL_HOST2, port: PORT2 }
]
```
1. Note that for each persistence class, GitLab will default to using the
configuration specified in `gitlab_rails['redis_sentinels']` unless
overriden by the settings above.
1. Be sure to include BOTH configuration options for each persistent classes. For example,
if you choose to configure a cache instance, you must specify both `gitlab_rails['redis_cache_instance']`
and `gitlab_rails['redis_cache_sentinels']` for GitLab to generate the proper configuration files.
1. Run `gitlab-ctl reconfigure`
### Control running services ### Control running services
In the previous example, we've used `redis_sentinel_role` and In the previous example, we've used `redis_sentinel_role` and
......
...@@ -119,11 +119,17 @@ The Pages daemon doesn't listen to the outside world. ...@@ -119,11 +119,17 @@ The Pages daemon doesn't listen to the outside world.
1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`: 1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`:
```ruby ```shell
pages_external_url 'http://example.io' pages_external_url 'http://example.io'
``` ```
1. [Reconfigure GitLab][reconfigure] 1. [Reconfigure GitLab][reconfigure]
1. Restart gitlab-pages by running the following command:
```shell
sudo gitlab-ctl restart gitlab-pages
```
Watch the [video tutorial][video-admin] for this configuration. Watch the [video tutorial][video-admin] for this configuration.
...@@ -143,7 +149,7 @@ outside world. ...@@ -143,7 +149,7 @@ outside world.
1. Place the certificate and key inside `/etc/gitlab/ssl` 1. Place the certificate and key inside `/etc/gitlab/ssl`
1. In `/etc/gitlab/gitlab.rb` specify the following configuration: 1. In `/etc/gitlab/gitlab.rb` specify the following configuration:
```ruby ```shell
pages_external_url 'https://example.io' pages_external_url 'https://example.io'
pages_nginx['redirect_http_to_https'] = true pages_nginx['redirect_http_to_https'] = true
...@@ -155,6 +161,11 @@ outside world. ...@@ -155,6 +161,11 @@ outside world.
respectively. respectively.
1. [Reconfigure GitLab][reconfigure] 1. [Reconfigure GitLab][reconfigure]
1. Restart gitlab-pages by running the following command:
```shell
sudo gitlab-ctl restart gitlab-pages
```
## Advanced configuration ## Advanced configuration
...@@ -180,7 +191,7 @@ world. Custom domains are supported, but no TLS. ...@@ -180,7 +191,7 @@ world. Custom domains are supported, but no TLS.
1. Edit `/etc/gitlab/gitlab.rb`: 1. Edit `/etc/gitlab/gitlab.rb`:
```ruby ```shell
pages_external_url "http://example.io" pages_external_url "http://example.io"
nginx['listen_addresses'] = ['1.1.1.1'] nginx['listen_addresses'] = ['1.1.1.1']
pages_nginx['enable'] = false pages_nginx['enable'] = false
...@@ -192,6 +203,11 @@ world. Custom domains are supported, but no TLS. ...@@ -192,6 +203,11 @@ world. Custom domains are supported, but no TLS.
listens on. If you don't have IPv6, you can omit the IPv6 address. listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure] 1. [Reconfigure GitLab][reconfigure]
1. Restart gitlab-pages by running the following command:
```shell
sudo gitlab-ctl restart gitlab-pages
```
### Custom domains with TLS support ### Custom domains with TLS support
...@@ -210,7 +226,7 @@ world. Custom domains and TLS are supported. ...@@ -210,7 +226,7 @@ world. Custom domains and TLS are supported.
1. Edit `/etc/gitlab/gitlab.rb`: 1. Edit `/etc/gitlab/gitlab.rb`:
```ruby ```shell
pages_external_url "https://example.io" pages_external_url "https://example.io"
nginx['listen_addresses'] = ['1.1.1.1'] nginx['listen_addresses'] = ['1.1.1.1']
pages_nginx['enable'] = false pages_nginx['enable'] = false
...@@ -225,6 +241,11 @@ world. Custom domains and TLS are supported. ...@@ -225,6 +241,11 @@ world. Custom domains and TLS are supported.
listens on. If you don't have IPv6, you can omit the IPv6 address. listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure] 1. [Reconfigure GitLab][reconfigure]
1. Restart gitlab-pages by running the following command:
```shell
sudo gitlab-ctl restart gitlab-pages
```
### Custom domain verification ### Custom domain verification
...@@ -247,11 +268,16 @@ are stored. ...@@ -247,11 +268,16 @@ are stored.
If you wish to store them in another location you must set it up in If you wish to store them in another location you must set it up in
`/etc/gitlab/gitlab.rb`: `/etc/gitlab/gitlab.rb`:
```ruby ```shell
gitlab_rails['pages_path'] = "/mnt/storage/pages" gitlab_rails['pages_path'] = "/mnt/storage/pages"
``` ```
1. [Reconfigure GitLab][reconfigure] 1. [Reconfigure GitLab][reconfigure]
1. Restart gitlab-pages by running the following command:
```shell
sudo gitlab-ctl restart gitlab-pages
```
## Set maximum pages size ## Set maximum pages size
......
...@@ -112,13 +112,13 @@ POST /projects/import ...@@ -112,13 +112,13 @@ POST /projects/import
| `file` | string | yes | The file to be uploaded | | `file` | string | yes | The file to be uploaded |
| `path` | string | yes | Name and path for new project | | `path` | string | yes | Name and path for new project |
| `overwrite` | boolean | no | If there is a project with the same path the import will overwrite it. Default to false | | `overwrite` | boolean | no | If there is a project with the same path the import will overwrite it. Default to false |
| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md)] | | `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md) |
The override params passed will take precendence over all values defined inside the export file. The override params passed will take precedence over all values defined inside the export file.
To upload a file from your filesystem, use the `--form` argument. This causes To upload a file from your file system, use the `--form` argument. This causes
cURL to post data using the header `Content-Type: multipart/form-data`. cURL to post data using the header `Content-Type: multipart/form-data`.
The `file=` parameter must point to a file on your filesystem and be preceded The `file=` parameter must point to a file on your file system and be preceded
by `@`. For example: by `@`. For example:
```console ```console
......
...@@ -1399,3 +1399,26 @@ Read more in the [Project Badges](project_badges.md) documentation. ...@@ -1399,3 +1399,26 @@ Read more in the [Project Badges](project_badges.md) documentation.
## Issue and merge request description templates ## Issue and merge request description templates
The non-default [issue and merge request description templates](../user/project/description_templates.md) are managed inside the project's repository. So you can manage them via the API through the [Repositories API](repositories.md) and the [Repository Files API](repository_files.md). The non-default [issue and merge request description templates](../user/project/description_templates.md) are managed inside the project's repository. So you can manage them via the API through the [Repositories API](repositories.md) and the [Repository Files API](repository_files.md).
## Download snapshot of a git repository
> Introduced in GitLab 10.7
This endpoint may only be accessed by an administrative user.
Download a snapshot of the project (or wiki, if requested) git repository. This
snapshot is always in uncompressed [tar](https://en.wikipedia.org/wiki/Tar_(computing))
format.
If a repository is corrupted to the point where `git clone` does not work, the
snapshot may allow some of the data to be retrieved.
```
GET /projects/:id/snapshot
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `wiki` | boolean | no | Whether to download the wiki, rather than project, repository |
...@@ -55,6 +55,7 @@ GET /users ...@@ -55,6 +55,7 @@ GET /users
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `order_by` | string | no | Return projects ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` | | `order_by` | string | no | Return projects ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `two_factor` | string | no | Filter users by Two-factor authentication. Filter values are `enabled` or `disabled`. By default it returns all users |
```json ```json
[ [
......
...@@ -22,7 +22,7 @@ The `merge_request` value is a reference to a merge request that adds this ...@@ -22,7 +22,7 @@ The `merge_request` value is a reference to a merge request that adds this
entry, and the `author` key is used to give attribution to community entry, and the `author` key is used to give attribution to community
contributors. **Both are optional**. contributors. **Both are optional**.
The `type` field maps the category of the change, The `type` field maps the category of the change,
valid options are: added, fixed, changed, deprecated, removed, security, other. **Type field is mandatory**. valid options are: added, fixed, changed, deprecated, removed, security, performance, other. **Type field is mandatory**.
Community contributors and core team members are encouraged to add their name to Community contributors and core team members are encouraged to add their name to
the `author` field. GitLab team members **should not**. the `author` field. GitLab team members **should not**.
......
...@@ -90,6 +90,25 @@ Finished in 34.51 seconds (files took 0.76702 seconds to load) ...@@ -90,6 +90,25 @@ Finished in 34.51 seconds (files took 0.76702 seconds to load)
Note: `live_debug` only works on javascript enabled specs. Note: `live_debug` only works on javascript enabled specs.
### Fast unit tests
Some classes are well-isolated from Rails and you should be able to test them
without the overhead added by the Rails environment and Bundler's `:default`
group's gem loading. In these cases, you can `require 'fast_spec_helper'`
instead of `require 'spec_helper'` in your test file, and your test should run
really fast since:
- Gems loading is skipped
- Rails app boot is skipped
- gitlab-shell and Gitaly setup are skipped
- Test repositories setup are skipped
Note that in some cases, you might have to add some `require_dependency 'foo'`
in your file under test since Rails autoloading is not available in these cases.
This shouldn't be a problem since explicitely listing dependencies should be
considered a good practice anyway.
### `let` variables ### `let` variables
GitLab's RSpec suite has made extensive use of `let` variables to reduce GitLab's RSpec suite has made extensive use of `let` variables to reduce
...@@ -281,14 +300,13 @@ All fixtures should be be placed under `spec/fixtures/`. ...@@ -281,14 +300,13 @@ All fixtures should be be placed under `spec/fixtures/`.
RSpec config files are files that change the RSpec config (i.e. RSpec config files are files that change the RSpec config (i.e.
`RSpec.configure do |config|` blocks). They should be placed under `RSpec.configure do |config|` blocks). They should be placed under
`spec/support/config/`. `spec/support/`.
Each file should be related to a specific domain, e.g. Each file should be related to a specific domain, e.g.
`spec/support/config/capybara.rb`, `spec/support/config/carrierwave.rb`, etc. `spec/support/capybara.rb`, `spec/support/carrierwave.rb`, etc.
Helpers can be included in the `spec/support/config/rspec.rb` file. If a If a helpers module applies only to a certain kind of specs, it should add
helpers module applies only to a certain kind of specs, it should add modifiers modifiers to the `config.include` call. For instance if
to the `config.include` call. For instance if
`spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and `spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and
`type: :model` specs only, you would write the following: `type: :model` specs only, you would write the following:
...@@ -299,6 +317,14 @@ RSpec.configure do |config| ...@@ -299,6 +317,14 @@ RSpec.configure do |config|
end end
``` ```
If a config file only consists of `config.include`, you can add these
`config.include` directly in `spec/spec_helper.rb`.
For very generic helpers, consider including them in the `spec/support/rspec.rb`
file which is used by the `spec/fast_spec_helper.rb` file. See
[Fast unit tests](#fast-unit-tests) for more details about the
`spec/fast_spec_helper.rb` file.
--- ---
[Return to Testing documentation](index.md) [Return to Testing documentation](index.md)
...@@ -9,7 +9,7 @@ At a minimum, requiring the Rake helper will redirect `stdout`, include the ...@@ -9,7 +9,7 @@ At a minimum, requiring the Rake helper will redirect `stdout`, include the
runtime task helpers, and include the `RakeHelpers` Spec support module. runtime task helpers, and include the `RakeHelpers` Spec support module.
The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make
executing tasks simple. See `spec/support/rake_helpers.rb` for all available executing tasks simple. See `spec/support/helpers/rake_helpers.rb` for all available
methods. methods.
Example: Example:
......
...@@ -23,7 +23,7 @@ You can create as many deploy tokens as you like from the settings of your proje ...@@ -23,7 +23,7 @@ You can create as many deploy tokens as you like from the settings of your proje
![Personal access tokens page](img/deploy_tokens.png) ![Personal access tokens page](img/deploy_tokens.png)
## Revoking a personal access token ## Revoking a deploy token
At any time, you can revoke any deploy token by just clicking the At any time, you can revoke any deploy token by just clicking the
respective **Revoke** button under the 'Active deploy tokens' area. respective **Revoke** button under the 'Active deploy tokens' area.
......
...@@ -12,7 +12,11 @@ end ...@@ -12,7 +12,11 @@ end
WebMock.enable! WebMock.enable!
%w(select2_helper test_env repo_helpers wait_for_requests sidekiq project_forks_helper webmock).each do |f| %w(select2_helper test_env repo_helpers wait_for_requests project_forks_helper).each do |f|
require Rails.root.join('spec', 'support', 'helpers', f)
end
%w(sidekiq webmock).each do |f|
require Rails.root.join('spec', 'support', f) require Rails.root.join('spec', 'support', f)
end end
......
...@@ -154,6 +154,7 @@ module API ...@@ -154,6 +154,7 @@ module API
mount ::API::ProjectHooks mount ::API::ProjectHooks
mount ::API::Projects mount ::API::Projects
mount ::API::ProjectMilestones mount ::API::ProjectMilestones
mount ::API::ProjectSnapshots
mount ::API::ProjectSnippets mount ::API::ProjectSnippets
mount ::API::ProtectedBranches mount ::API::ProtectedBranches
mount ::API::Repositories mount ::API::Repositories
......
...@@ -64,8 +64,10 @@ module API ...@@ -64,8 +64,10 @@ module API
authorize! :create_note, noteable authorize! :create_note, noteable
parent = noteable_parent(noteable) parent = noteable_parent(noteable)
if opts[:created_at] if opts[:created_at]
opts.delete(:created_at) unless current_user.admin? || parent.owner == current_user opts.delete(:created_at) unless
current_user.admin? || parent.owned_by?(current_user)
end end
project = parent if parent.is_a?(Project) project = parent if parent.is_a?(Project)
......
module API
module Helpers
module ProjectSnapshotsHelpers
def authorize_read_git_snapshot!
authenticated_with_full_private_access!
end
def send_git_snapshot(repository)
header(*Gitlab::Workhorse.send_git_snapshot(repository))
end
def snapshot_project
user_project
end
def snapshot_repository
if to_boolean(params[:wiki])
snapshot_project.wiki.repository
else
snapshot_project.repository
end
end
end
end
end
module API
class ProjectSnapshots < Grape::API
helpers ::API::Helpers::ProjectSnapshotsHelpers
before { authorize_read_git_snapshot! }
resource :projects do
desc 'Download a (possibly inconsistent) snapshot of a repository' do
detail 'This feature was introduced in GitLab 10.7'
end
params do
optional :wiki, type: Boolean, desc: 'Set to true to receive the wiki repository'
end
get ':id/snapshot' do
send_git_snapshot(snapshot_repository)
end
end
end
end
...@@ -74,6 +74,11 @@ module API ...@@ -74,6 +74,11 @@ module API
present options[:with].prepare_relation(projects, options), options present options[:with].prepare_relation(projects, options), options
end end
def translate_params_for_compatibility(params)
params[:builds_enabled] = params.delete(:jobs_enabled) if params.key?(:jobs_enabled)
params
end
end end
resource :users, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do resource :users, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
...@@ -123,7 +128,7 @@ module API ...@@ -123,7 +128,7 @@ module API
end end
post do post do
attrs = declared_params(include_missing: false) attrs = declared_params(include_missing: false)
attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled) attrs = translate_params_for_compatibility(attrs)
project = ::Projects::CreateService.new(current_user, attrs).execute project = ::Projects::CreateService.new(current_user, attrs).execute
if project.saved? if project.saved?
...@@ -155,6 +160,7 @@ module API ...@@ -155,6 +160,7 @@ module API
not_found!('User') unless user not_found!('User') unless user
attrs = declared_params(include_missing: false) attrs = declared_params(include_missing: false)
attrs = translate_params_for_compatibility(attrs)
project = ::Projects::CreateService.new(user, attrs).execute project = ::Projects::CreateService.new(user, attrs).execute
if project.saved? if project.saved?
...@@ -276,7 +282,7 @@ module API ...@@ -276,7 +282,7 @@ module API
authorize! :rename_project, user_project if attrs[:name].present? authorize! :rename_project, user_project if attrs[:name].present?
authorize! :change_visibility_level, user_project if attrs[:visibility].present? authorize! :change_visibility_level, user_project if attrs[:visibility].present?
attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled) attrs = translate_params_for_compatibility(attrs)
result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
......
...@@ -77,7 +77,7 @@ module API ...@@ -77,7 +77,7 @@ module API
authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?) authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?)
unless current_user&.admin? unless current_user&.admin?
params.except!(:created_after, :created_before, :order_by, :sort) params.except!(:created_after, :created_before, :order_by, :sort, :two_factor)
end end
users = UsersFinder.new(current_user, params).execute users = UsersFinder.new(current_user, params).execute
......
require_dependency 'gitlab/git' require_dependency 'settings'
require_dependency 'gitlab/popen'
module Gitlab module Gitlab
def self.root
Pathname.new(File.expand_path('..', __dir__))
end
def self.config
Settings
end
COM_URL = 'https://gitlab.com'.freeze COM_URL = 'https://gitlab.com'.freeze
APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))} APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z} SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}
VERSION = File.read(root.join("VERSION")).strip.freeze
REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp.freeze
def self.com? def self.com?
# Check `gl_subdomain?` as well to keep parity with gitlab.com # Check `gl_subdomain?` as well to keep parity with gitlab.com
......
require_dependency 'gitlab/encoding_helper'
module Gitlab module Gitlab
module Git module Git
# The ID of empty tree. # The ID of empty tree.
......
module Gitlab module Gitlab
module Wiki module Git
class CommitterWithHooks < Gollum::Committer class CommitterWithHooks < Gollum::Committer
attr_reader :gl_wiki attr_reader :gl_wiki
...@@ -9,6 +9,9 @@ module Gitlab ...@@ -9,6 +9,9 @@ module Gitlab
end end
def commit def commit
# TODO: Remove after 10.8
return super unless allowed_to_run_hooks?
result = Gitlab::Git::OperationService.new(git_user, gl_wiki.repository).with_branch( result = Gitlab::Git::OperationService.new(git_user, gl_wiki.repository).with_branch(
@wiki.ref, @wiki.ref,
start_branch_name: @wiki.ref start_branch_name: @wiki.ref
...@@ -24,6 +27,11 @@ module Gitlab ...@@ -24,6 +27,11 @@ module Gitlab
private private
# TODO: Remove after 10.8
def allowed_to_run_hooks?
@options[:user_id] != 0 && @options[:username].present?
end
def git_user def git_user
@git_user ||= Gitlab::Git::User.new(@options[:username], @git_user ||= Gitlab::Git::User.new(@options[:username],
@options[:name], @options[:name],
......
...@@ -1258,6 +1258,10 @@ module Gitlab ...@@ -1258,6 +1258,10 @@ module Gitlab
true true
end end
def create_from_snapshot(url, auth)
gitaly_repository_client.create_from_snapshot(url, auth)
end
def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:) def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
gitaly_migrate(:rebase) do |is_enabled| gitaly_migrate(:rebase) do |is_enabled|
if is_enabled if is_enabled
......
...@@ -290,7 +290,7 @@ module Gitlab ...@@ -290,7 +290,7 @@ module Gitlab
end end
def committer_with_hooks(commit_details) def committer_with_hooks(commit_details)
Gitlab::Wiki::CommitterWithHooks.new(self, commit_details.to_h) Gitlab::Git::CommitterWithHooks.new(self, commit_details.to_h)
end end
def with_committer_with_hooks(commit_details, &block) def with_committer_with_hooks(commit_details, &block)
......
...@@ -235,6 +235,22 @@ module Gitlab ...@@ -235,6 +235,22 @@ module Gitlab
) )
end end
def create_from_snapshot(http_url, http_auth)
request = Gitaly::CreateRepositoryFromSnapshotRequest.new(
repository: @gitaly_repo,
http_url: http_url,
http_auth: http_auth
)
GitalyClient.call(
@storage,
:repository_service,
:create_repository_from_snapshot,
request,
timeout: GitalyClient.default_timeout
)
end
def write_ref(ref_path, ref, old_ref, shell) def write_ref(ref_path, ref, old_ref, shell)
request = Gitaly::WriteRefRequest.new( request = Gitaly::WriteRefRequest.new(
repository: @gitaly_repo, repository: @gitaly_repo,
......
...@@ -27,8 +27,6 @@ project_tree: ...@@ -27,8 +27,6 @@ project_tree:
- :releases - :releases
- project_members: - project_members:
- :user - :user
- lfs_file_locks:
- :user
- merge_requests: - merge_requests:
- notes: - notes:
- :author - :author
......
...@@ -81,6 +81,20 @@ module Gitlab ...@@ -81,6 +81,20 @@ module Gitlab
] ]
end end
def send_git_snapshot(repository)
params = {
'GitalyServer' => gitaly_server_hash(repository),
'GetSnapshotRequest' => Gitaly::GetSnapshotRequest.new(
repository: repository.gitaly_repository
).to_json
}
[
SEND_DATA_HEADER,
"git-snapshot:#{encode(params)}"
]
end
def send_git_diff(repository, diff_refs) def send_git_diff(repository, diff_refs)
params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
{ {
......
require 'settingslogic'
class Settings < Settingslogic
source ENV.fetch('GITLAB_CONFIG') { Pathname.new(File.expand_path('..', __dir__)).join('config/gitlab.yml') }
namespace ENV.fetch('GITLAB_ENV') { Rails.env }
class << self
def gitlab_on_standard_port?
on_standard_port?(gitlab)
end
def host_without_www(url)
host(url).sub('www.', '')
end
def build_gitlab_ci_url
custom_port =
if on_standard_port?(gitlab)
nil
else
":#{gitlab.port}"
end
[
gitlab.protocol,
"://",
gitlab.host,
custom_port,
gitlab.relative_url_root
].join('')
end
def build_pages_url
base_url(pages).join('')
end
def build_gitlab_shell_ssh_path_prefix
user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
if gitlab_shell.ssh_port != 22
"ssh://#{user_host}:#{gitlab_shell.ssh_port}/"
else
if gitlab_shell.ssh_host.include? ':'
"[#{user_host}]:"
else
"#{user_host}:"
end
end
end
def build_base_gitlab_url
base_url(gitlab).join('')
end
def build_gitlab_url
(base_url(gitlab) + [gitlab.relative_url_root]).join('')
end
# check that values in `current` (string or integer) is a contant in `modul`.
def verify_constant_array(modul, current, default)
values = default || []
unless current.nil?
values = []
current.each do |constant|
values.push(verify_constant(modul, constant, nil))
end
values.delete_if { |value| value.nil? }
end
values
end
# check that `current` (string or integer) is a contant in `modul`.
def verify_constant(modul, current, default)
constant = modul.constants.find { |name| modul.const_get(name) == current }
value = constant.nil? ? default : modul.const_get(constant)
if current.is_a? String
value = modul.const_get(current.upcase) rescue default
end
value
end
def absolute(path)
File.expand_path(path, Rails.root)
end
private
def base_url(config)
custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
[
config.protocol,
"://",
config.host,
custom_port
]
end
def on_standard_port?(config)
config.port.to_i == (config.https ? 443 : 80)
end
# Extract the host part of the given +url+.
def host(url)
url = url.downcase
url = "http://#{url}" unless url.start_with?('http')
# Get rid of the path so that we don't even have to encode it
url_without_path = url.sub(%r{(https?://[^/]+)/?.*}, '\1')
URI.parse(url_without_path).host
end
# Runs every minute in a random ten-minute period on Sundays, to balance the
# load on the server receiving these pings. The usage ping is safe to run
# multiple times because of a 24 hour exclusive lock.
def cron_for_usage_ping
hour = rand(24)
minute = rand(6)
"#{minute}0-#{minute}9 #{hour} * * 0"
end
end
end
...@@ -1776,6 +1776,9 @@ msgstr "" ...@@ -1776,6 +1776,9 @@ msgstr ""
msgid "Failed to change the owner" msgid "Failed to change the owner"
msgstr "" msgstr ""
msgid "Failed to check related branches."
msgstr ""
msgid "Failed to remove issue from board, please try again." msgid "Failed to remove issue from board, please try again."
msgstr "" msgstr ""
......
module RuboCop module RuboCop
module SpecHelpers module SpecHelpers
SPEC_HELPERS = %w[spec_helper.rb rails_helper.rb].freeze SPEC_HELPERS = %w[fast_spec_helper.rb rails_helper.rb spec_helper.rb].freeze
# Returns true if the given node originated from the spec directory. # Returns true if the given node originated from the spec directory.
def in_spec?(node) def in_spec?(node)
......
...@@ -223,11 +223,12 @@ describe Import::BitbucketController do ...@@ -223,11 +223,12 @@ describe Import::BitbucketController do
end end
context 'user has chosen an existing nested namespace and name for the project', :postgresql do context 'user has chosen an existing nested namespace and name for the project', :postgresql do
let(:parent_namespace) { create(:group, name: 'foo', owner: user) } let(:parent_namespace) { create(:group, name: 'foo') }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) } let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
let(:test_name) { 'test_name' } let(:test_name) { 'test_name' }
before do before do
parent_namespace.add_owner(user)
nested_namespace.add_owner(user) nested_namespace.add_owner(user)
end end
...@@ -273,7 +274,7 @@ describe Import::BitbucketController do ...@@ -273,7 +274,7 @@ describe Import::BitbucketController do
context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do
let(:test_name) { 'test_name' } let(:test_name) { 'test_name' }
let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } let!(:parent_namespace) { create(:group, name: 'foo') }
before do before do
parent_namespace.add_owner(user) parent_namespace.add_owner(user)
......
...@@ -196,10 +196,11 @@ describe Import::GitlabController do ...@@ -196,10 +196,11 @@ describe Import::GitlabController do
end end
context 'user has chosen an existing nested namespace for the project', :postgresql do context 'user has chosen an existing nested namespace for the project', :postgresql do
let(:parent_namespace) { create(:group, name: 'foo', owner: user) } let(:parent_namespace) { create(:group, name: 'foo') }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) } let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
before do before do
parent_namespace.add_owner(user)
nested_namespace.add_owner(user) nested_namespace.add_owner(user)
end end
...@@ -245,7 +246,7 @@ describe Import::GitlabController do ...@@ -245,7 +246,7 @@ describe Import::GitlabController do
context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do
let(:test_name) { 'test_name' } let(:test_name) { 'test_name' }
let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } let!(:parent_namespace) { create(:group, name: 'foo') }
before do before do
parent_namespace.add_owner(user) parent_namespace.add_owner(user)
......
...@@ -4,7 +4,11 @@ describe Projects::ForksController do ...@@ -4,7 +4,11 @@ describe Projects::ForksController do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
let(:forked_project) { Projects::ForkService.new(project, user).execute } let(:forked_project) { Projects::ForkService.new(project, user).execute }
let(:group) { create(:group, owner: forked_project.creator) } let(:group) { create(:group) }
before do
group.add_owner(user)
end
describe 'GET index' do describe 'GET index' do
def get_forks def get_forks
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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