Commit fb3e23b0 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into ide-staged-changes

parents 35d28045 9c2f6e04
No related merge requests found
<!--
# Read me first!
Create this issue under https://dev.gitlab.org/gitlab/gitlabhq
Set the title to: `[Security] Description of the original issue`
-->
### Prior to the security release
- [ ] Read the [security process for developers] if you are not familiar with it.
- [ ] Link to the original issue adding it to the [links section](#links)
- [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org`
- [ ] Create an MR targetting `org` `master`, prefixing your branch with `security-`
- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]`
- [ ] Add a link to the MR to the [links section](#links)
- [ ] Add a link to an EE MR if required
- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
- [ ] Assign the MR to a RM once is reviewed and ready to be merged. Check the [RM list] to see who to ping.
#### Backports
- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases
- [ ] At this point, it might be easy to squash the commits from the MR into one
- You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [seckpick documentation]
- [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable)
- [ ] Create each MR targetting the security branch `security-X-Y`
- [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/process.md#secpick-script
#### Documentation and final details
- [ ] Check the topic on #security to see when the next release is going ot happen and add a link to the [links section](#links)
- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
### Summary
#### Links
| Description | Link |
| -------- | -------- |
| Original issue | #TODO |
| Security release issue | #TODO |
| `master` MR | !TODO |
| `master` MR (EE) | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR (EE) | !TODO |
| `Backport X.Y` MR (EE) | !TODO |
| `Backport X.Y` MR (EE) | !TODO |
#### Details
| Description | Details | Further details|
| -------- | -------- | -------- |
| Versions affected | X.Y | |
| Upgrade notes | | |
| GitLab Settings updated | Yes/No| |
| Migration required | Yes/No | |
| Thanks | | |
[security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md
[RM list]: https://about.gitlab.com/release-managers/
/label ~security
......@@ -62,7 +62,7 @@ gem 'akismet', '~> 2.0'
# Two-factor authentication
gem 'devise-two-factor', '~> 3.0.0'
gem 'rqrcode-rails3', '~> 0.1.7'
gem 'attr_encrypted', '~> 3.0.0'
gem 'attr_encrypted', '~> 3.1.0'
gem 'u2f', '~> 0.2.1'
# GitLab Pages
......
......@@ -66,7 +66,7 @@ GEM
unf
ast (2.4.0)
atomic (1.1.99)
attr_encrypted (3.0.3)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.0)
autoprefixer-rails (6.2.3)
......@@ -1004,7 +1004,7 @@ DEPENDENCIES
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.2.0)
attr_encrypted (~> 3.0.0)
attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
......
......@@ -16,6 +16,7 @@ class DeleteModal {
bindEvents() {
this.$toggleBtns.on('click', this.setModalData.bind(this));
this.$confirmInput.on('input', this.setDeleteDisabled.bind(this));
this.$deleteBtn.on('click', this.setDisableDeleteButton.bind(this));
}
setModalData(e) {
......@@ -30,6 +31,16 @@ class DeleteModal {
this.$deleteBtn.attr('disabled', e.currentTarget.value !== this.branchName);
}
setDisableDeleteButton(e) {
if (this.$deleteBtn.is('[disabled]')) {
e.preventDefault();
e.stopPropagation();
return false;
}
return true;
}
updateModal() {
this.$branchName.text(this.branchName);
this.$confirmInput.val('');
......
<script>
import _ from 'underscore';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import {
APPLICATION_INSTALLED,
INGRESS,
} from '../constants';
import _ from 'underscore';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { APPLICATION_INSTALLED, INGRESS } from '../constants';
export default {
components: {
applicationRow,
clipboardButton,
export default {
components: {
applicationRow,
clipboardButton,
},
props: {
applications: {
type: Object,
required: false,
default: () => ({}),
},
props: {
applications: {
type: Object,
required: false,
default: () => ({}),
},
helpPath: {
type: String,
required: false,
default: '',
},
ingressHelpPath: {
type: String,
required: false,
default: '',
},
ingressDnsHelpPath: {
type: String,
required: false,
default: '',
},
managePrometheusPath: {
type: String,
required: false,
default: '',
},
helpPath: {
type: String,
required: false,
default: '',
},
computed: {
generalApplicationDescription() {
return sprintf(
_.escape(s__(
ingressHelpPath: {
type: String,
required: false,
default: '',
},
ingressDnsHelpPath: {
type: String,
required: false,
default: '',
},
managePrometheusPath: {
type: String,
required: false,
default: '',
},
},
computed: {
generalApplicationDescription() {
return sprintf(
_.escape(
s__(
`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`,
)), {
helpLink: `<a href="${this.helpPath}">
),
),
{
helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`,
},
false,
);
},
ingressId() {
return INGRESS;
},
ingressInstalled() {
return this.applications.ingress.status === APPLICATION_INSTALLED;
},
ingressExternalIp() {
return this.applications.ingress.externalIp;
},
ingressDescription() {
const extraCostParagraph = sprintf(
_.escape(s__(
},
false,
);
},
ingressId() {
return INGRESS;
},
ingressInstalled() {
return this.applications.ingress.status === APPLICATION_INSTALLED;
},
ingressExternalIp() {
return this.applications.ingress.externalIp;
},
ingressDescription() {
const extraCostParagraph = sprintf(
_.escape(
s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources
like a load balancer, which may incur additional costs depending on
the hosting provider your Kubernetes cluster is installed on. If you are using GKE,
you can %{pricingLink}.`,
)), {
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
the hosting provider your Kubernetes cluster is installed on. If you are using
Google Kubernetes Engine, you can %{pricingLink}.`,
),
),
{
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`,
},
false,
);
},
false,
);
const externalIpParagraph = sprintf(
_.escape(s__(
const externalIpParagraph = sprintf(
_.escape(
s__(
`ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS
at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`,
)), {
ingressHelpLink: `<a href="${this.ingressHelpPath}">
),
),
{
ingressHelpLink: `<a href="${this.ingressHelpPath}">
${_.escape(s__('ClusterIntegration|More information'))}
</a>`,
},
false,
);
},
false,
);
return `
return `
<p>
${extraCostParagraph}
</p>
......@@ -98,22 +104,25 @@
${externalIpParagraph}
</p>
`;
},
prometheusDescription() {
return sprintf(
_.escape(s__(
},
prometheusDescription() {
return sprintf(
_.escape(
s__(
`ClusterIntegration|Prometheus is an open-source monitoring system
with %{gitlabIntegrationLink} to monitor deployed applications.`,
)), {
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
),
),
{
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`,
},
false,
);
},
},
false,
);
},
};
},
};
</script>
<template>
......@@ -205,7 +214,7 @@
>
{{ s__(`ClusterIntegration|The IP address is in
the process of being assigned. Please check your Kubernetes
cluster or Quotas on GKE if it takes a long time.`) }}
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }}
<a
:href="ingressHelpPath"
......
<script>
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
export default {
components: {
icon,
Icon,
},
directives: {
tooltip,
......
<script>
import { mapState } from 'vuex';
import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
import { mapState } from 'vuex';
import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
export default {
components: {
RadioGroup,
export default {
components: {
RadioGroup,
},
computed: {
...mapState(['currentBranchId']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
{ branchName: `<strong class="monospace">${this.currentBranchId}</strong>` },
false,
);
},
computed: {
...mapState([
'currentBranchId',
]),
newMergeRequestHelpText() {
return sprintf(
__('Creates a new branch from %{branchName} and re-directs to create a new merge request'),
{ branchName: this.currentBranchId },
);
},
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
{ branchName: `<strong>${this.currentBranchId}</strong>` },
false,
);
},
commitToNewBranchText() {
return sprintf(
__('Creates a new branch from %{branchName}'),
{ branchName: this.currentBranchId },
);
},
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
};
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
};
</script>
<template>
......@@ -53,13 +39,11 @@
:value="$options.commitToNewBranch"
:label="__('Create a new branch')"
:show-input="true"
:help-text="commitToNewBranchText"
/>
<radio-group
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
:show-input="true"
:help-text="newMergeRequestHelpText"
/>
</div>
</template>
<script>
import { __, sprintf } from '../../../locale';
import Icon from '../../../vue_shared/components/icon.vue';
import popover from '../../../vue_shared/directives/popover';
import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
export default {
directives: {
popover,
},
components: {
Icon,
},
props: {
text: {
type: String,
required: true,
},
},
data() {
return {
scrollTop: 0,
isFocused: false,
};
},
computed: {
allLines() {
return this.text.split('\n').map((line, i) => ({
text: line.substr(0, this.getLineLength(i)) || ' ',
highlightedText: line.substr(this.getLineLength(i)),
}));
},
},
methods: {
handleScroll() {
if (this.$refs.textarea) {
this.$nextTick(() => {
this.scrollTop = this.$refs.textarea.scrollTop;
});
}
},
getLineLength(i) {
return i === 0 ? MAX_TITLE_LENGTH : MAX_BODY_LENGTH;
},
onInput(e) {
this.$emit('input', e.target.value);
},
updateIsFocused(isFocused) {
this.isFocused = isFocused;
},
},
popoverOptions: {
trigger: 'hover',
placement: 'top',
content: sprintf(
__(`
The character highligher helps you keep the subject line to %{titleLength} characters
and wrap the body at %{bodyLength} so they are readable in git.
`),
{ titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
),
},
};
</script>
<template>
<fieldset class="common-note-form ide-commit-message-field">
<div
class="md-area"
:class="{
'is-focused': isFocused
}"
>
<div
v-once
class="md-header"
>
<ul class="nav-links">
<li>
{{ __('Commit Message') }}
<span
v-popover="$options.popoverOptions"
class="help-block prepend-left-10"
>
<icon
name="question"
/>
</span>
</li>
</ul>
</div>
<div class="ide-commit-message-textarea-container">
<div class="ide-commit-message-highlights-container">
<div
class="note-textarea highlights monospace"
:style="{
transform: `translate3d(0, ${-scrollTop}px, 0)`
}"
>
<div
v-for="(line, index) in allLines"
:key="index"
>
<span
v-text="line.text"
>
</span><mark
v-show="line.highlightedText"
v-text="line.highlightedText"
>
</mark>
</div>
</div>
</div>
<textarea
class="note-textarea ide-commit-message-textarea"
name="commit-message"
:placeholder="__('Write a commit message...')"
:value="text"
@scroll="handleScroll"
@input="onInput"
@focus="updateIsFocused(true)"
@blur="updateIsFocused(false)"
ref="textarea"
>
</textarea>
</div>
</div>
</fieldset>
</template>
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import { mapActions, mapState, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
export default {
directives: {
tooltip,
},
props: {
value: {
type: String,
required: true,
},
props: {
value: {
type: String,
required: true,
},
label: {
type: String,
required: false,
default: null,
},
checked: {
type: Boolean,
required: false,
default: false,
},
showInput: {
type: Boolean,
required: false,
default: false,
},
helpText: {
type: String,
required: false,
default: null,
},
label: {
type: String,
required: false,
default: null,
},
computed: {
...mapState('commit', [
'commitAction',
]),
...mapGetters('commit', [
'newBranchName',
]),
checked: {
type: Boolean,
required: false,
default: false,
},
methods: {
...mapActions('commit', [
'updateCommitAction',
'updateBranchName',
]),
showInput: {
type: Boolean,
required: false,
default: false,
},
};
},
computed: {
...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']),
},
methods: {
...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
},
};
</script>
<template>
......@@ -65,18 +53,6 @@
{{ label }}
</template>
<slot v-else></slot>
<span
v-if="helpText"
v-tooltip
class="help-block inline"
:title="helpText"
>
<i
class="fa fa-question-circle"
aria-hidden="true"
>
</i>
</span>
</span>
</label>
<div
......@@ -85,7 +61,7 @@
>
<input
type="text"
class="form-control"
class="form-control monospace"
:placeholder="newBranchName"
@input="updateBranchName($event.target.value)"
/>
......
......@@ -6,8 +6,9 @@ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import commitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
import Actions from './commit_sidebar/actions.vue';
import CommitMessageField from './commit_sidebar/message_field.vue';
import * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue';
export default {
components: {
......@@ -17,6 +18,7 @@ export default {
EmptyState,
Actions,
LoadingButton,
CommitMessageField,
},
directives: {
tooltip,
......@@ -92,16 +94,10 @@ export default {
@submit.prevent.stop="commitChanges"
v-if="!rightPanelCollapsed"
>
<div class="multi-file-commit-fieldset">
<textarea
class="form-control multi-file-commit-message"
name="commit-message"
:value="commitMessage"
:placeholder="__('Write a commit message...')"
@input="updateCommitMessage($event.target.value)"
>
</textarea>
</div>
<commit-message-field
:text="commitMessage"
@input="updateCommitMessage"
/>
<div class="clearfix prepend-top-15">
<actions />
<loading-button
......
// Fuzzy file finder
export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72;
......@@ -36,11 +36,11 @@ const router = new VueRouter({
base: `${gon.relative_url_root}/-/ide/`,
routes: [
{
path: '/project/:namespace/:project',
path: '/project/:namespace/:project+',
component: EmptyRouterComponent,
children: [
{
path: ':targetmode/:branch/*',
path: ':targetmode(edit|tree|blob)/:branch/*',
component: EmptyRouterComponent,
},
{
......
......@@ -5,45 +5,71 @@ import * as types from '../mutation_types';
export const getProjectData = (
{ commit, state, dispatch },
{ namespace, projectId, force = false } = {},
) => new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, { entry: state });
service.getProjectData(namespace, projectId)
.then(res => res.data)
.then((data) => {
) =>
new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, { entry: state });
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data);
})
.catch(() => {
flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
});
} else {
resolve(state.projects[`${namespace}/${projectId}`]);
}
});
service
.getProjectData(namespace, projectId)
.then(res => res.data)
.then(data => {
commit(types.TOGGLE_LOADING, { entry: state });
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
if (!state.currentProjectId)
commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data);
})
.catch(() => {
flash(
'Error loading project data. Please try again.',
'alert',
document,
null,
false,
true,
);
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
});
} else {
resolve(state.projects[`${namespace}/${projectId}`]);
}
});
export const getBranchData = (
{ commit, state, dispatch },
{ projectId, branchId, force = false } = {},
) => new Promise((resolve, reject) => {
if ((typeof state.projects[`${projectId}`] === 'undefined' ||
!state.projects[`${projectId}`].branches[branchId])
|| force) {
service.getBranchData(`${projectId}`, branchId)
.then(({ data }) => {
const { id } = data.commit;
commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
resolve(data);
})
.catch(() => {
flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
) =>
new Promise((resolve, reject) => {
if (
typeof state.projects[`${projectId}`] === 'undefined' ||
!state.projects[`${projectId}`].branches[branchId] ||
force
) {
service
.getBranchData(`${projectId}`, branchId)
.then(({ data }) => {
const { id } = data.commit;
commit(types.SET_BRANCH, {
projectPath: `${projectId}`,
branchName: branchId,
branch: data,
});
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
commit(types.SET_CURRENT_BRANCH, branchId);
resolve(data);
})
.catch(() => {
flash(
'Error loading branch data. Please try again.',
'alert',
document,
null,
false,
true,
);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
......@@ -17,12 +17,8 @@ export default {
});
},
[types.SET_DIRECTORY_DATA](state, { data, treePath }) {
Object.assign(state, {
trees: Object.assign(state.trees, {
[treePath]: {
tree: data,
},
}),
Object.assign(state.trees[treePath], {
tree: data,
});
},
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
......
......@@ -19,7 +19,6 @@ import AjaxCache from '~/lib/utils/ajax_cache';
import Vue from 'vue';
import syntaxHighlight from '~/syntax_highlight';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
......@@ -198,6 +197,8 @@ export default class Notes {
);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff-retry-button', this.onClickRetryLazyLoad.bind(this));
// fetch notes when tab becomes visible
this.$wrapperEl.on('visibilitychange', this.visibilityChange);
// when issue status changes, we need to refresh data
......@@ -244,6 +245,7 @@ export default class Notes {
this.$wrapperEl.off('click', '.js-comment-resolve-button');
this.$wrapperEl.off('click', '.system-note-commit-list-toggler');
this.$wrapperEl.off('click', '.js-toggle-lazy-diff');
this.$wrapperEl.off('click', '.js-toggle-lazy-diff-retry-button');
this.$wrapperEl.off('ajax:success', '.js-main-target-form');
this.$wrapperEl.off('ajax:success', '.js-discussion-note-form');
this.$wrapperEl.off('ajax:complete', '.js-main-target-form');
......@@ -1431,16 +1433,15 @@ export default class Notes {
syntaxHighlight(fileHolder);
}
static renderDiffError($container) {
$container.find('.line_content').html(
$(`
<div class="nothing-here-block">
${__(
'Unable to load the diff.',
)} <a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>?
</div>
`),
);
onClickRetryLazyLoad(e) {
const $retryButton = $(e.currentTarget);
$retryButton.prop('disabled', true);
return this.loadLazyDiff(e)
.then(() => {
$retryButton.prop('disabled', false);
});
}
loadLazyDiff(e) {
......@@ -1449,20 +1450,35 @@ export default class Notes {
$container.find('.js-toggle-lazy-diff').removeClass('js-toggle-lazy-diff');
const tableEl = $container.find('tbody');
if (tableEl.length === 0) return;
const $tableEl = $container.find('tbody');
if ($tableEl.length === 0) return;
const fileHolder = $container.find('.file-holder');
const url = fileHolder.data('linesPath');
axios
const $errorContainer = $container.find('.js-error-lazy-load-diff');
const $successContainer = $container.find('.js-success-lazy-load');
/**
* We only fetch resolved discussions.
* Unresolved discussions don't have an endpoint being provided.
*/
if (url) {
return axios
.get(url)
.then(({ data }) => {
// Reset state in case last request returned error
$successContainer.removeClass('hidden');
$errorContainer.addClass('hidden');
Notes.renderDiffContent($container, data);
})
.catch(() => {
Notes.renderDiffError($container);
$successContainer.addClass('hidden');
$errorContainer.removeClass('hidden');
});
}
return Promise.resolve();
}
toggleCommitList(e) {
......
......@@ -10,29 +10,25 @@ export default class PerformanceBarService {
}
static registerInterceptor(peekUrl, callback) {
vueResourceInterceptor = (request, next) => {
next(response => {
const requestId = response.headers['x-request-id'];
const requestUrl = response.url;
if (requestUrl !== peekUrl && requestId) {
callback(requestId, requestUrl);
}
});
};
Vue.http.interceptors.push(vueResourceInterceptor);
return axios.interceptors.response.use(response => {
const interceptor = response => {
const requestId = response.headers['x-request-id'];
const requestUrl = response.config.url;
// Get the request URL from response.config for Axios, and response for
// Vue Resource.
const requestUrl = (response.config || response).url;
const cachedResponse = response.headers['x-gitlab-from-cache'] === 'true';
if (requestUrl !== peekUrl && requestId) {
if (requestUrl !== peekUrl && requestId && !cachedResponse) {
callback(requestId, requestUrl);
}
return response;
});
};
vueResourceInterceptor = (request, next) => next(interceptor);
Vue.http.interceptors.push(vueResourceInterceptor);
return axios.interceptors.response.use(interceptor);
}
static removeInterceptor(interceptor) {
......
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetPipelineBlocked',
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
</span>
</div>
</div>
`,
};
<script>
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'PipelineFailed',
components: {
statusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="warning"
:show-disabled-button="true"
/>
<div class="media-body space-children">
<span class="bold">
{{ s__(`mrWidget|The pipeline for this merge request failed.
Please retry the job or push a new commit to fix the failure`) }}
</span>
</div>
</div>
</template>
......@@ -31,7 +31,7 @@ export { default as ReadyToMergeState } from './components/states/ready_to_merge
export { default as ShaMismatchState } from './components/states/sha_mismatch.vue';
export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
export { default as PipelineFailedState } from './components/states/pipeline_failed.vue';
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue';
......
......@@ -175,7 +175,7 @@
</a>
</span>
<span v-else>
Cant find HEAD commit for this branch
Can't find HEAD commit for this branch
</span>
</div>
</div>
......
......@@ -503,3 +503,7 @@ fieldset[disabled] .btn,
@extend %disabled;
}
}
.btn-no-padding {
padding: 0;
}
......@@ -180,10 +180,6 @@
justify-content: space-between;
align-items: center;
flex-grow: 1;
.merge-request-branches & {
flex-direction: column;
}
}
.commit-content {
......
......@@ -160,6 +160,11 @@
}
}
}
.diff-loading-error-block {
padding: $gl-padding * 2 $gl-padding;
text-align: center;
}
}
.image {
......
......@@ -762,3 +762,20 @@
max-width: 100%;
}
}
// Hack alert: we've rewritten `btn` class in a way that
// we've broken it and it is not possible to use with `btn-link`
// which causes a blank button when it's disabled and hovering
// The css in here is the boostrap one
.btn-link-retry {
&[disabled] {
cursor: not-allowed;
box-shadow: none;
opacity: .65;
&:hover {
color: $file-mode-changed;
text-decoration: none;
}
}
}
......@@ -14,6 +14,11 @@
.commit-title {
margin: 0;
white-space: normal;
@media (max-width: $screen-sm-max) {
justify-content: flex-end;
}
}
.ci-table {
......
......@@ -680,11 +680,6 @@
}
}
.multi-file-commit-message.form-control {
height: 160px;
resize: none;
}
.dirty-diff {
// !important need to override monaco inline style
width: 4px !important;
......@@ -874,3 +869,74 @@
align-items: center;
font-weight: $gl-font-weight-bold;
}
.ide-commit-message-field {
height: 200px;
background-color: $white-light;
.md-area {
display: flex;
flex-direction: column;
height: 100%;
}
.nav-links {
height: 30px;
}
.help-block {
margin-top: 2px;
color: $blue-500;
cursor: pointer;
}
}
.ide-commit-message-textarea-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
.note-textarea {
font-family: $monospace_font;
}
}
.ide-commit-message-highlights-container {
position: absolute;
left: 0;
top: 0;
right: -100px;
bottom: 0;
padding-right: 100px;
pointer-events: none;
z-index: 1;
.highlights {
white-space: pre-wrap;
word-wrap: break-word;
color: transparent;
}
mark {
margin-left: -1px;
padding: 0 2px;
border-radius: $border-radius-small;
background-color: $orange-200;
color: transparent;
opacity: 0.6;
}
}
.ide-commit-message-textarea {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 2;
background: transparent;
resize: none;
}
......@@ -5,6 +5,7 @@ class ApplicationController < ActionController::Base
include Gitlab::GonHelper
include GitlabRoutingHelper
include PageLayoutHelper
include SafeParamsHelper
include SentryHelper
include WorkhorseHelper
include EnforcesTwoFactorAuthentication
......
......@@ -217,7 +217,7 @@ module NotesActions
def note_project
strong_memoize(:note_project) do
return nil unless project
next nil unless project
note_project_id = params[:note_project_id]
......@@ -228,7 +228,7 @@ module NotesActions
project
end
return access_denied! unless can?(current_user, :create_note, the_project)
next access_denied! unless can?(current_user, :create_note, the_project)
the_project
end
......
......@@ -86,7 +86,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
out_of_range = todos.current_page > total_pages
if out_of_range
redirect_to url_for(params.merge(page: total_pages, only_path: true))
redirect_to url_for(safe_params.merge(page: total_pages, only_path: true))
end
out_of_range
......
......@@ -15,7 +15,7 @@ module Groups
def update
if @group.update(group_variables_params)
respond_to do |format|
format.json { return render_group_variables }
format.json { render_group_variables }
end
else
respond_to do |format|
......
......@@ -189,6 +189,6 @@ class GroupsController < Groups::ApplicationController
params[:id] = group.to_param
url_for(params)
url_for(safe_params)
end
end
......@@ -60,13 +60,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
format.patch do
return render_404 unless @merge_request.diff_refs
break render_404 unless @merge_request.diff_refs
send_git_patch @project.repository, @merge_request.diff_refs
end
format.diff do
return render_404 unless @merge_request.diff_refs
break render_404 unless @merge_request.diff_refs
send_git_diff @project.repository, @merge_request.diff_refs
end
......
......@@ -12,7 +12,7 @@ class Projects::VariablesController < Projects::ApplicationController
def update
if @project.update(variables_params)
respond_to do |format|
format.json { return render_variables }
format.json { render_variables }
end
else
respond_to do |format|
......
......@@ -404,7 +404,7 @@ class ProjectsController < Projects::ApplicationController
params[:namespace_id] = project.namespace.to_param
params[:id] = project.to_param
url_for(params)
url_for(safe_params)
end
def project_export_enabled
......
......@@ -146,6 +146,6 @@ class UsersController < ApplicationController
end
def build_canonical_path(user)
url_for(params.merge(username: user.to_param))
url_for(safe_params.merge(username: user.to_param))
end
end
......@@ -259,7 +259,7 @@ module BlobHelper
options = []
if error == :collapsed
options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, expanded: true, format: nil)))
options << link_to('load it anyway', url_for(safe_params.merge(viewer: viewer.type, expanded: true, format: nil)))
end
# If the error is `:server_side_but_stored_externally`, the simple viewer will show the same error,
......
......@@ -180,7 +180,7 @@ module DiffHelper
private
def diff_btn(title, name, selected)
params_copy = params.dup
params_copy = safe_params.dup
params_copy[:view] = name
# Always use HTML to handle case where JSON diff rendered this button
......
module SafeParamsHelper
# Rails 5.0 requires to permit `params` if they're used in url helpers.
# Use this helper when generating links with `params.merge(...)`
def safe_params
if params.respond_to?(:permit!)
params.except(:host, :port, :protocol).permit!
else
params
end
end
end
......@@ -90,7 +90,7 @@ module TreeHelper
end
def commit_in_single_accessible_branch
branch_name = html_escape(selected_branch)
branch_name = ERB::Util.html_escape(selected_branch)
message = _("Your changes can be committed to %{branch_name} because a merge "\
"request is open.") % { branch_name: "<strong>#{branch_name}</strong>" }
......
......@@ -6,6 +6,12 @@ module Emails
mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason))
end
def issue_due_email(recipient_id, issue_id, reason = nil)
setup_issue_mail(issue_id, recipient_id)
mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason))
end
def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
......
......@@ -162,7 +162,7 @@ module Ci
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
end
before_transition pending: :running do |build|
after_transition pending: :running do |build|
build.ensure_metadata.update_timeout_state
end
end
......@@ -479,7 +479,7 @@ module Ci
def user_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
return variables if user.blank?
break variables if user.blank?
variables.append(key: 'GITLAB_USER_ID', value: user.id.to_s)
variables.append(key: 'GITLAB_USER_EMAIL', value: user.email)
......@@ -594,7 +594,7 @@ module Ci
def persisted_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
return variables unless persisted?
break variables unless persisted?
variables
.append(key: 'CI_JOB_ID', value: id.to_s)
......@@ -643,7 +643,7 @@ module Ci
def persisted_environment_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
return variables unless persisted? && persisted_environment.present?
break variables unless persisted? && persisted_environment.present?
variables.concat(persisted_environment.predefined_variables)
......
......@@ -4,7 +4,7 @@ module Ci
include HasVariable
include Presentable
belongs_to :group
belongs_to :group, class_name: "::Group"
alias_attribute :secret_value, :value
......
......@@ -87,7 +87,7 @@ class CommitStatus < ActiveRecord::Base
transition [:created, :pending, :running, :manual] => :canceled
end
before_transition created: [:pending, :running] do |commit_status|
before_transition [:created, :skipped, :manual] => :pending do |commit_status|
commit_status.queued_at = Time.now
end
......
......@@ -11,7 +11,9 @@ module CacheMarkdownField
extend ActiveSupport::Concern
# Increment this number every time the renderer changes its output
CACHE_VERSION = 3
CACHE_REDCARPET_VERSION = 3
CACHE_COMMONMARK_VERSION_START = 10
CACHE_COMMONMARK_VERSION = 11
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
......@@ -49,12 +51,14 @@ module CacheMarkdownField
# Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project)
group = self.group if self.respond_to?(:group)
group = self.group if self.respond_to?(:group)
context = cached_markdown_fields[field].merge(project: project, group: group)
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
context[:markdown_engine] = markdown_engine
context
end
......@@ -69,7 +73,7 @@ module CacheMarkdownField
Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
]
end.to_h
updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
updates['cached_markdown_version'] = latest_cached_markdown_version
updates.each {|html_field, data| write_attribute(html_field, data) }
end
......@@ -90,7 +94,7 @@ module CacheMarkdownField
markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_field) || false
CacheMarkdownField::CACHE_VERSION == cached_markdown_version &&
latest_cached_markdown_version == cached_markdown_version &&
(html_changed || markdown_changed == html_changed)
end
......@@ -109,6 +113,24 @@ module CacheMarkdownField
__send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend
end
def latest_cached_markdown_version
return CacheMarkdownField::CACHE_REDCARPET_VERSION unless cached_markdown_version
if cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
CacheMarkdownField::CACHE_REDCARPET_VERSION
else
CacheMarkdownField::CACHE_COMMONMARK_VERSION
end
end
def markdown_engine
if latest_cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
:redcarpet
else
:common_mark
end
end
included do
cattr_reader :cached_markdown_fields do
FieldData.new
......
......@@ -49,6 +49,7 @@ class Issue < ActiveRecord::Base
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
scope :due_tomorrow, -> { where(due_date: Date.tomorrow) }
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
......
......@@ -83,14 +83,14 @@ class NotificationRecipient
def has_access?
DeclarativePolicy.subject_scope do
return false unless user.can?(:receive_notifications)
return true if @skip_read_ability
break false unless user.can?(:receive_notifications)
break true if @skip_read_ability
return false if @target && !user.can?(:read_cross_project)
return false if @project && !user.can?(:read_project, @project)
break false if @target && !user.can?(:read_cross_project)
break false if @project && !user.can?(:read_project, @project)
return true unless read_ability
return true unless DeclarativePolicy.has_policy?(@target)
break true unless read_ability
break true unless DeclarativePolicy.has_policy?(@target)
user.can?(read_ability, @target)
end
......
......@@ -47,7 +47,8 @@ class NotificationSetting < ActiveRecord::Base
].freeze
EXCLUDED_WATCHER_EVENTS = [
:push_to_merge_request
:push_to_merge_request,
:issue_due
].push(*EXCLUDED_PARTICIPATING_EVENTS).freeze
def self.find_or_create_for(source)
......
......@@ -1637,7 +1637,7 @@ class Project < ActiveRecord::Base
def container_registry_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
return variables unless Gitlab.config.registry.enabled
break variables unless Gitlab.config.registry.enabled
variables.append(key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port)
......
......@@ -331,6 +331,7 @@ class Repository
return unless empty?
expire_method_caches(%i(has_visible_content?))
raw_repository.expire_has_local_branches_cache
end
def lookup_cache
......
......@@ -4,6 +4,9 @@ module Ci
class RegisterJobService
attr_reader :runner
JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30].freeze
JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET = 5.freeze
Result = Struct.new(:build, :valid?)
def initialize(runner)
......@@ -30,7 +33,7 @@ module Ci
end
end
builds.find do |build|
builds.auto_include(false).find do |build|
next unless runner.can_pick?(build)
begin
......@@ -41,7 +44,7 @@ module Ci
build.run!
register_success(build)
return Result.new(build, true)
return Result.new(build, true) # rubocop:disable Cop/AvoidReturnFromBlocks
rescue Ci::Build::MissingDependenciesError
build.drop!(:missing_dependency_failure)
end
......@@ -104,10 +107,22 @@ module Ci
end
def register_success(job)
job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at)
labels = { shared_runner: runner.shared?,
jobs_running_for_project: jobs_running_for_project(job) }
job_queue_duration_seconds.observe(labels, Time.now - job.queued_at) unless job.queued_at.nil?
attempt_counter.increment
end
def jobs_running_for_project(job)
return '+Inf' unless runner.shared?
# excluding currently started job
running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.shared)
.limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1
running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
end
def failed_attempt_counter
@failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job")
end
......@@ -117,7 +132,7 @@ module Ci
end
def job_queue_duration_seconds
@job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time')
@job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time', {}, JOB_QUEUE_DURATION_SECONDS_BUCKETS)
end
end
end
......@@ -13,7 +13,7 @@ module Clusters
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
rescue ActiveRecord::RecordInvalid => e
provider.make_errored!("Failed to configure GKE Cluster: #{e.message}")
provider.make_errored!("Failed to configure Google Kubernetes Engine Cluster: #{e.message}")
end
private
......
......@@ -17,7 +17,7 @@ module Clusters
when 'DONE'
finalize_creation
else
return provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
end
end
end
......
......@@ -19,8 +19,8 @@ class CreateDeploymentService
environment.fire_state_event(action)
return unless environment.save
return if environment.stopped?
break unless environment.save
break if environment.stopped?
deploy.tap(&:update_merge_request_metrics!)
end
......
......@@ -10,7 +10,7 @@ class ImportExportCleanUpService
def execute
Gitlab::Metrics.measure(:import_export_clean_up) do
return unless File.directory?(path)
next unless File.directory?(path)
clean_up_export_files
end
......
......@@ -203,10 +203,11 @@ module NotificationRecipientService
attr_reader :action
attr_reader :previous_assignee
attr_reader :skip_current_user
def initialize(target, current_user, action:, previous_assignee: nil, skip_current_user: true)
def initialize(target, current_user, action:, custom_action: nil, previous_assignee: nil, skip_current_user: true)
@target = target
@current_user = current_user
@action = action
@custom_action = custom_action
@previous_assignee = previous_assignee
@skip_current_user = skip_current_user
end
......@@ -236,7 +237,13 @@ module NotificationRecipientService
add_mentions(current_user, target: target)
# Add the assigned users, if any
assignees = custom_action == :new_issue ? target.assignees : target.assignee
assignees = case custom_action
when :new_issue
target.assignees
else
target.assignee
end
# We use the `:participating` notification level in order to match existing legacy behavior as captured
# in existing specs (notification_service_spec.rb ~ line 507)
add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees
......
......@@ -373,6 +373,20 @@ class NotificationService
end
end
def issue_due(issue)
recipients = NotificationRecipientService.build_recipients(
issue,
issue.author,
action: 'due',
custom_action: :issue_due,
skip_current_user: false
)
recipients.each do |recipient|
mailer.send(:issue_due_email, recipient.user.id, issue.id, recipient.reason).deliver_later
end
end
protected
def new_resource_email(target, method)
......
......@@ -137,7 +137,7 @@ module Projects
return true unless Gitlab.config.registry.enabled
ContainerRepository.build_root_repository(project).tap do |repository|
return repository.has_tags? ? repository.delete_tags! : true
break repository.has_tags? ? repository.delete_tags! : true
end
end
......
......@@ -10,7 +10,7 @@ class RepositoryArchiveCleanUpService
def execute
Gitlab::Metrics.measure(:repository_archive_clean_up) do
return unless File.directory?(path)
next unless File.directory?(path)
clean_up_old_archives
clean_up_empty_directories
......
......@@ -159,7 +159,7 @@ module SystemNoteService
body = if noteable.time_estimate == 0
"removed time estimate"
else
"changed time estimate to #{parsed_time}"
"changed time estimate to #{parsed_time},"
end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
......
......@@ -19,7 +19,7 @@ module TestHooks
error_message = catch(:validation_error) do
sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend
return hook.execute(sample_data, trigger_key)
return hook.execute(sample_data, trigger_key) # rubocop:disable Cop/AvoidReturnFromBlocks
end
error(error_message)
......
xml.title "#{current_user.name} issues"
xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
......
......@@ -2,12 +2,12 @@
- page_title _("Issues")
- @breadcrumb_link = issues_dashboard_path(assignee_id: current_user.id)
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues")
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues")
.top-area
= render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set
.nav-controls
= link_to params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do
= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do
= icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
......
......@@ -7,7 +7,7 @@
- unless expanded
- diff_data = { lines_path: project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion) }
.diff-file.file-holder{ class: diff_file_class, data: diff_data }
.diff-file.file-holder.js-lazy-load-discussion{ class: diff_file_class, data: diff_data }
.js-file-title.file-title.file-title-flex-parent
.file-header-content
= render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false
......@@ -28,8 +28,11 @@
%tr.line_holder.line-holder-placeholder
%td.old_line.diff-line-num
%td.new_line.diff-line-num
%td.line_content
%td.line_content.js-success-lazy-load
.js-code-placeholder
%td.js-error-lazy-load-diff.hidden.diff-loading-error-block
- button = button_tag(_("Try again"), class: "btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button")
= _("Unable to load the diff. %{button_try_again}").html_safe % { button_try_again: button}
= render "discussions/diff_discussion", discussions: [discussion], expanded: true
- else
- partial = (diff_file.new_file? || diff_file.deleted_file?) ? 'single_image_diff' : 'replaced_image_diff'
......
xml.title "#{@group.name} issues"
xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: issues_group_url, rel: "alternate", type: "text/html"
xml.id issues_group_url
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
......
- page_title "Issues"
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues")
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues")
- if group_issues_count(state: 'all').zero?
= render 'shared/empty_states/issues', project_select_button: true
......
%p.details
#{link_to @issue.author_name, user_url(@issue.author)}'s issue is due soon.
- if @issue.assignees.any?
%p
Assignee: #{@issue.assignee_list}
%p
This issue is due on: #{@issue.due_date.to_s(:medium)}
- if @issue.description
%div
= markdown(@issue.description, pipeline: :email, author: @issue.author)
The following issue is due on <%= @issue.due_date %>:
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= @issue.author_name %>
Assignee: <%= @issue.assignee_list %>
<%= @issue.description %>
......@@ -4,7 +4,7 @@
- load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?)
- external_embed = local_assigns.fetch(:external_embed, false)
- viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async
- viewer_url = local_assigns.fetch(:viewer_url) { url_for(safe_params.merge(viewer: viewer.type, format: :json)) } if load_async
.blob-viewer{ data: { type: viewer.type, rich_type: rich_type, url: viewer_url }, class: ('hidden' if hidden) }
- if render_error
= render 'projects/blob/render_error', viewer: viewer
......
......@@ -29,7 +29,7 @@
= s_('Branches|Cant find HEAD commit for this branch')
- if branch.name != @repository.root_ref
.divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
.divergence-graph.hidden-xs{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
default_branch: @repository.root_ref,
number_commits_ahead: diverging_count_label(number_commits_ahead) } }
.graph-side
......
......@@ -8,6 +8,6 @@
%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration')
%p= s_('ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab')
= link_to s_('ClusterIntegration|Create on GKE'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
= link_to s_('ClusterIntegration|Create on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
%p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
= link_to s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
......@@ -22,7 +22,7 @@
= author_avatar(commit, size: 36)
.commit-detail.flex-list
.commit-content
.commit-content.qa-commit-content
- if view_details && merge_request
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title"
- else
......
- diff_file = viewer.diff_file
- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
- url = url_for(safe_params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
.nothing-here-block.diff-collapsed{ data: { diff_for_path: url } }
This diff is collapsed.
%a.click-to-expand Click to expand it.
= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
= link_to safe_params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss')
- if @can_bulk_update
= button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
......
xml.title "#{@project.name} issues"
xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: project_issues_url(@project), rel: "alternate", type: "text/html"
xml.id project_issues_url(@project)
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
......
......@@ -5,7 +5,7 @@
- new_issue_email = @project.new_issuable_address(current_user, 'issue')
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@project.name} issues")
- if project_issues(@project).exists?
%div{ class: (container_class) }
......
......@@ -3,7 +3,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f|
.hide.alert.alert-danger.mr-compare-errors
.merge-request-branches.js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
.js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
.col-md-6
.panel.panel-default.panel-new-merge-request
.panel-heading
......
......@@ -26,16 +26,16 @@
- else
%ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.commits-tab.active
= link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
= link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
Commits
%span.badge= @commits.size
- if @pipelines.any?
%li.builds-tab
= link_to url_for(params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
= link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
Pipelines
%span.badge= @pipelines.size
%li.diffs-tab
= link_to url_for(params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
= link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
Changes
%span.badge= @merge_request.diff_size
......@@ -46,7 +46,7 @@
-# This tab is always loaded via AJAX
- if @pipelines.any?
#pipelines.pipelines.tab-pane
= render 'projects/merge_requests/pipelines', endpoint: url_for(params.merge(action: 'pipelines', format: :json)), disable_initialization: true
= render 'projects/merge_requests/pipelines', endpoint: url_for(safe_params.merge(action: 'pipelines', format: :json)), disable_initialization: true
.mr-loading-status
= spinner
- breadcrumb_title "Pipelines"
- page_title "New Pipeline"
- page_title = s_("Pipeline|Run Pipeline")
%h3.page-title
New Pipeline
= s_("Pipeline|Run Pipeline")
%hr
= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f|
= form_errors(@pipeline)
.form-group
= f.label :ref, 'Create for', class: 'control-label'
= f.label :ref, s_('Pipeline|Run on'), class: 'control-label'
.col-sm-10
= hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide git-revision-dropdown-toggle',
filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: "Search branches",
filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"),
data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
.help-block Existing branch name, tag
.help-block
= s_("Pipeline|Existing branch name, tag")
.form-actions
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-cancel'
= f.submit s_('Pipeline|Run pipeline'), class: 'btn btn-success', tabindex: 3
= link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default pull-right'
-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
......@@ -7,8 +7,8 @@
- content_for :push_access_levels do
.push_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-push wide',
dropdown_class: 'dropdown-menu-selectable capitalize-header',
options: { toggle_class: 'js-allowed-to-push qa-allowed-to-push-select wide',
dropdown_class: 'dropdown-menu-selectable qa-allowed-to-push-dropdown capitalize-header',
data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
= render 'projects/protected_branches/shared/create_protected_branch'
......@@ -6,5 +6,5 @@
%td
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level
= dropdown_tag( (protected_branch.push_access_levels.first.humanize || 'Select') ,
options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
options: { toggle_class: 'js-allowed-to-push qa-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: protected_branch.push_access_levels.first.id }})
.protected-branches-list.js-protected-branches-list
.protected-branches-list.js-protected-branches-list.qa-protected-branches-list
- if @protected_branches.empty?
.panel-heading
%h3.panel-title
......
= f.hidden_field(:name)
= dropdown_tag('Select branch or create wildcard',
options: { toggle_class: 'js-protected-branch-select js-filter-submit wide git-revision-dropdown-toggle',
filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: "Search protected branches",
options: { toggle_class: 'js-protected-branch-select js-filter-submit wide git-revision-dropdown-toggle qa-protected-branch-select',
filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown qa-protected-branch-dropdown", placeholder: "Search protected branches",
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_branch_name],
......
......@@ -4,7 +4,7 @@
.settings-header
%h4
Protected Branches
%button.btn.js-settings-toggle{ type: 'button' }
%button.btn.js-settings-toggle.qa-expand-protected-branches{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
......
......@@ -2,7 +2,7 @@
%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) } }
%td
%span.ref-name= protected_branch.name
%span.ref-name.qa-protected-branch-name= protected_branch.name
- if @project.root_ref?(protected_branch.name)
%span.label.label-info.prepend-left-5 default
......
......@@ -8,8 +8,8 @@
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
.dropdown
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" }
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown qa-branches-select" }
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging.qa-branches-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
.dropdown-page-one
= dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags")
......
......@@ -18,6 +18,7 @@
- cronjob:stuck_import_jobs
- cronjob:stuck_merge_jobs
- cronjob:trending_projects
- cronjob:issue_due_scheduler
- gcp_cluster:cluster_install_app
- gcp_cluster:cluster_provision
......@@ -39,6 +40,8 @@
- github_importer:github_import_stage_import_pull_requests
- github_importer:github_import_stage_import_repository
- mail_scheduler:mail_scheduler_issue_due
- object_storage_upload
- object_storage:object_storage_background_move
- object_storage:object_storage_migrate_uploads
......
module MailSchedulerQueue
extend ActiveSupport::Concern
included do
queue_namespace :mail_scheduler
end
end
class IssueDueSchedulerWorker
include ApplicationWorker
include CronjobQueue
def perform
project_ids = Issue.opened.due_tomorrow.group(:project_id).pluck(:project_id).map { |id| [id] }
MailScheduler::IssueDueWorker.bulk_perform_async(project_ids)
end
end
module MailScheduler
class IssueDueWorker
include ApplicationWorker
include MailSchedulerQueue
def perform(project_id)
notification_service = NotificationService.new
Issue.opened.due_tomorrow.in_projects(project_id).preload(:project).find_each do |issue|
notification_service.issue_due(issue)
end
end
end
end
......@@ -33,7 +33,7 @@ class PostReceive
unless @user
log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
return false
return false # rubocop:disable Cop/AvoidReturnFromBlocks
end
if Gitlab::Git.tag_ref?(ref)
......
......@@ -38,7 +38,7 @@ class StuckCiJobsWorker
def drop_stuck(status, timeout)
search(status, timeout) do |build|
return unless build.stuck?
break unless build.stuck?
drop_build :stuck, build, status, timeout
end
......
#!/usr/bin/env ruby
require 'optparse'
require 'open3'
require 'rainbow/refinement'
using Rainbow
BRANCH_PREFIX = 'security'.freeze
STABLE_BRANCH_SUFFIX = 'stable'.freeze
REMOTE = 'dev'.freeze
options = { version: nil, branch: nil, sha: nil }
parser = OptionParser.new do |opts|
opts.banner = "Usage: #{$0} [options]"
opts.on('-v', '--version 10.0', 'Version') do |version|
options[:version] = version&.tr('.', '-')
end
opts.on('-b', '--branch security-fix-branch', 'Original branch name') do |branch|
options[:branch] = branch
end
opts.on('-s', '--sha abcd', 'SHA to cherry pick') do |sha|
options[:sha] = sha
end
opts.on('-h', '--help', 'Displays Help') do
puts opts
exit
end
end
parser.parse!
abort("Missing options. Use #{$0} --help to see the list of options available".red) if options.values.include?(nil)
abort("Wrong version format #{options[:version].bold}".red) unless options[:version] =~ /\A\d*\-\d*\Z/
branch = [BRANCH_PREFIX, options[:branch], options[:version]].join('-').freeze
stable_branch = "#{options[:version]}-#{STABLE_BRANCH_SUFFIX}".freeze
command = "git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}"
_stdin, stdout, stderr = Open3.popen3(command)
puts stdout.read&.green
puts stderr.read&.red
---
title: Add cron job to email users on issue due date
merge_request: 17985
author: Stuart Nelson
type: added
---
title: Improves wording in new pipeline page
merge_request:
author:
type: other
---
title: Add a comma to the time estimate system notes
merge_request: 18326
author:
type: changed
---
title: Remove ahead/behind graphs on project branches on mobile
merge_request: 18415
author: Takuya Noguchi
type: other
---
title: Rubocop rule to avoid returning from a block
merge_request: 18000
author: Jacopo Beschi @jacopo-beschi
type: added
---
title: Breaks commit not found message in pipelines table
merge_request:
author:
type: fixed
---
title: Replace GKE acronym with Google Kubernetes Engine
merge_request:
author:
type: other
---
title: Fix `Trace::HttpIO` can not render multi-byte chars
merge_request: 18417
author:
type: fixed
---
title: Fix confirmation modal for deleting a protected branch
merge_request: 18176
author: Paul Bonaud @PaulRbR
type: fixed
---
title: Fixes unresolved discussions rendering the error state instead of the diff
merge_request:
author:
type: fixed
---
title: '[API] Fix URLs in the `Link` header for `GET /projects/:id/repository/contributors`
when no value is passed for `order_by` or `sort`'
merge_request: 18393
author:
type: fixed
---
title: Fix undefined `html_escape` method during markdown rendering
merge_request: 18418
author:
type: fixed
---
title: Fix a case with secret variables being empty sometimes
merge_request: 18400
author:
type: fixed
---
title: Fixed IDE not loading for sub groups
merge_request:
author:
type: fixed
---
title: Fixed IDE not showing loading state when tree is loading
merge_request:
author:
type: fixed
---
title: Partition job_queue_duration_seconds with jobs_running_for_project
merge_request: 17730
author:
type: changed
---
title: Move PipelineFailed vue component
merge_request: 18277
author: George Tsiolis
type: performance
---
title: Memoize Git::Repository#has_visible_content?
merge_request:
author:
type: performance
---
title: Check if a ref exists is done by Gitaly by default
merge_request:
author:
type: performance
......@@ -455,6 +455,10 @@ Settings.cron_jobs['pages_domain_verification_cron_worker'] ||= Settingslogic.ne
Settings.cron_jobs['pages_domain_verification_cron_worker']['cron'] ||= '*/15 * * * *'
Settings.cron_jobs['pages_domain_verification_cron_worker']['job_class'] = 'PagesDomainVerificationCronWorker'
Settings.cron_jobs['issue_due_scheduler_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['issue_due_scheduler_worker']['cron'] ||= '50 00 * * *'
Settings.cron_jobs['issue_due_scheduler_worker']['job_class'] = 'IssueDueSchedulerWorker'
#
# Sidekiq
#
......
......@@ -27,16 +27,8 @@ module Sidekiq
Use an `after_commit` hook, or include `AfterCommitQueue` and use a `run_after_commit` block instead.
MSG
rescue Sidekiq::Worker::EnqueueFromTransactionError => e
if Rails.env.production?
Rails.logger.error(e.message)
if Gitlab::Sentry.enabled?
Gitlab::Sentry.context
Raven.capture_exception(e)
end
else
raise
end
Rails.logger.error(e.message) if Rails.env.production?
Gitlab::Sentry.track_exception(e)
end
end
......
......@@ -34,6 +34,7 @@
- [email_receiver, 2]
- [emails_on_push, 2]
- [mailers, 2]
- [mail_scheduler, 2]
- [invalid_gpg_signature_update, 2]
- [create_gpg_signature, 2]
- [rebase, 2]
......
class AddIssueDueToNotificationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :notification_settings, :issue_due, :boolean
end
end
......@@ -1325,6 +1325,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
t.boolean "failed_pipeline"
t.boolean "success_pipeline"
t.boolean "push_to_merge_request"
t.boolean "issue_due"
end
add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
......
......@@ -80,6 +80,7 @@ on projects and code.
- [Search through GitLab](user/search/index.md): Search for issues, merge requests, projects, groups, todos, and issues in Issue Boards.
- [Snippets](user/snippets.md): Snippets allow you to create little bits of code.
- [Wikis](user/project/wiki/index.md): Enhance your repository documentation with built-in wikis.
- [Web IDE](user/project/web_ide/index.md)
#### Repositories
......
# Administrator documentation
# Administrator documentation **[CORE ONLY]**
Learn how to administer your GitLab instance (Community Edition and
Enterprise Edition).
......
......@@ -23,6 +23,7 @@ new_issue
reopen_issue
close_issue
reassign_issue
issue_due
new_merge_request
push_to_merge_request
reopen_merge_request
......@@ -75,6 +76,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `reopen_issue` | boolean | no | Enable/disable this notification |
| `close_issue` | boolean | no | Enable/disable this notification |
| `reassign_issue` | boolean | no | Enable/disable this notification |
| `issue_due` | boolean | no | Enable/disable this notification |
| `new_merge_request` | boolean | no | Enable/disable this notification |
| `push_to_merge_request` | boolean | no | Enable/disable this notification |
| `reopen_merge_request` | boolean | no | Enable/disable this notification |
......@@ -142,6 +144,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `reopen_issue` | boolean | no | Enable/disable this notification |
| `close_issue` | boolean | no | Enable/disable this notification |
| `reassign_issue` | boolean | no | Enable/disable this notification |
| `issue_due` | boolean | no | Enable/disable this notification |
| `new_merge_request` | boolean | no | Enable/disable this notification |
| `push_to_merge_request` | boolean | no | Enable/disable this notification |
| `reopen_merge_request` | boolean | no | Enable/disable this notification |
......@@ -166,6 +169,7 @@ Example responses:
"reopen_issue": false,
"close_issue": false,
"reassign_issue": false,
"issue_due": false,
"new_merge_request": false,
"push_to_merge_request": false,
"reopen_merge_request": false,
......
......@@ -183,7 +183,7 @@ GET /projects/:id/repository/contributors
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `order_by` (optional) - Return contributors ordered by `name`, `email`, or `commits` fields. If not given contributors are ordered by commit date.
- `order_by` (optional) - Return contributors ordered by `name`, `email`, or `commits` (orders by commit date) fields. Default is `commits`
- `sort` (optional) - Return contributors sorted in `asc` or `desc` order. Default is `asc`
Response:
......
......@@ -157,6 +157,39 @@ below.
Otherwise, leave this mention out.
### Product badges
When a feature is available in EE-only tiers, add the corresponding tier according to the
feature availability:
- For GitLab Starter and GitLab.com Bronze: `**[STARTER]**`
- For GitLab Premium and GitLab.com Silver: `**[PREMIUM]**`
- For GitLab Ultimate and GitLab.com Gold: `**[ULTIMATE]**`
- For GitLab Core and GitLab.com Free: `**[CORE]**`
To exclude GitLab.com tiers (when the feature is not available in GitLab.com), add the
keyword "only":
- For GitLab Starter: `**[STARTER ONLY]**`
- For GitLab Premium: `**[PREMIUM ONLY]**`
- For GitLab Ultimate: `**[ULTIMATE ONLY]**`
- For GitLab Core: `**[CORE ONLY]**`
The tier should be ideally added to headers, so that the full badge will be displayed.
But it can be also mentioned from paragraphs, list items, and table cells. For these cases,
the tier mention will be represented by an orange question mark.
E.g., `**[STARTER]**` renders **[STARTER]**, `**[STARTER ONLY]**` renders **[STARTER ONLY]**.
The absence of tiers' mentions mean that the feature is available in GitLab Core,
GitLab.com Free, and higher tiers.
#### How it works
Introduced by [!244](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/244),
the special markup `**[STARTER]**` will generate a `span` element to trigger the
badges and tooltips (`<span class="badge-trigger starter">`). When the keyword
"only" is added, the corresponding GitLab.com badge will not be displayed.
### GitLab Restart
There are many cases that a restart/reconfigure of GitLab is required. To
......
......@@ -23,6 +23,7 @@ are very appreciative of the work done by translators and proofreaders!
- Italian
- Paolo Falomo - [GitLab](https://gitlab.com/paolofalomo), [Crowdin](https://crowdin.com/profile/paolo.falomo)
- Japanese
- Yamana Tokiuji - [GitLab](https://gitlab.com/tokiuji), [Crowdin](https://crowdin.com/profile/yamana)
- Korean
- Chang-Ho Cha - [GitLab](https://gitlab.com/changho-cha), [Crowdin](https://crowdin.com/profile/zzazang)
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
......
......@@ -31,8 +31,8 @@ the hardware requirements.
- [Install GitLab on DC/OS](https://mesosphere.com/blog/gitlab-dcos/) via [GitLab-Mesosphere integration](https://about.gitlab.com/2016/09/16/announcing-gitlab-and-mesosphere/)
- [Install GitLab on Azure](azure/index.md)
- [Install GitLab on Google Cloud Platform](google_cloud_platform/index.md)
- [Install GitLab on Google Container Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/): video tutorial on
the full process of installing GitLab on Google Container Engine (GKE), pushing an application to GitLab, building the app with GitLab CI/CD, and deploying to production.
- [Install GitLab on Google Kubernetes Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/): video tutorial on
the full process of installing GitLab on Google Kubernetes Engine (GKE), pushing an application to GitLab, building the app with GitLab CI/CD, and deploying to production.
- [Install on AWS](https://about.gitlab.com/aws/)
- _Testing only!_ [DigitalOcean and Docker Machine](digitaloceandocker.md) -
Quickly test any version of GitLab on DigitalOcean using Docker Machine.
......
......@@ -93,9 +93,9 @@ Is the system packaged Git too old? Remove it and compile from source.
# Download and compile from source
cd /tmp
curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.16.2.tar.gz
echo '9acc4339b7a2ab484eea69d705923271682b7058015219cf5a7e6ed8dee5b5fb git-2.16.2.tar.gz' | shasum -a256 -c - && tar -xzf git-2.16.2.tar.gz
cd git-2.16.2/
curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.16.3.tar.gz
echo 'dda229e9c73f4fbb7d4324e0d993e11311673df03f73b194c554c2e9451e17cd git-2.16.3.tar.gz' | shasum -a256 -c - && tar -xzf git-2.16.3.tar.gz
cd git-2.16.3/
./configure
make prefix=/usr/local all
......
......@@ -245,10 +245,7 @@ To enable this feature, navigate to the group settings page. Select
![Checkbox for share with group lock](img/share_with_group_lock.png)
#### Member Lock
> Available in [GitLab Starter](https://about.gitlab.com/products/) and
[GitLab.com Bronze](https://about.gitlab.com/gitlab-com/).
#### Member Lock **[STARTER]**
With **Member Lock** it is possible to lock membership in project to the
level of members in group.
......@@ -259,8 +256,8 @@ Learn more about [Member Lock](https://docs.gitlab.com/ee/user/group/index.html#
- **Projects**: view all projects within that group, add members to each project,
access each project's settings, and remove any project from the same screen.
- **Webhooks**: configure [webhooks](../project/integrations/webhooks.md)
and [push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html#push-rules) to your group (Push Rules is available in [GitLab Starter](https://about.gitlab.com/products/).)
- **Webhooks**: configure [webhooks](../project/integrations/webhooks.md) to your group.
- **Push rules**: configure [push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html#push-rules) to your group. **[STARTER]**
- **Audit Events**: view [Audit Events](https://docs.gitlab.com/ee/administration/audit_events.html#audit-events)
for the group (GitLab admins only, available in [GitLab Starter][ee]).
for the group. **[STARTER ONLY]**
- **Pipelines quota**: keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group
......@@ -17,7 +17,7 @@ When you create a project in GitLab, you'll have access to a large number of
- [Issue tracker](issues/index.md): Discuss implementations with your team within issues
- [Issue Boards](issue_board.md): Organize and prioritize your workflow
- [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards) (**Starter/Premium**): Allow your teams to create their own workflows (Issue Boards) for the same project
- [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards): Allow your teams to create their own workflows (Issue Boards) for the same project **[STARTER]**
- [Repositories](repository/index.md): Host your code in a fully
integrated platform
- [Branches](repository/branches/index.md): use Git branching strategies to
......@@ -30,8 +30,8 @@ integrated platform
- [Deploy tokens](deploy_tokens/index.md): Manage project-based deploy tokens that allow permanent access to the repository and Container Registry.
- [Merge Requests](merge_requests/index.md): Apply your branching
strategy and get reviewed by your team
- [Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) (**Starter/Premium**): Ask for approval before
implementing a change
- [Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html): Ask for approval before
implementing a change **[STARTER]**
- [Fix merge conflicts from the UI](merge_requests/resolve_conflicts.md):
Your Git diff tool right from GitLab's UI
- [Review Apps](../../ci/review_apps/index.md): Live preview the results
......
......@@ -35,5 +35,9 @@ Due dates also appear in your [todos list](../../../workflow/todos.md).
![Issues with due dates in the todos](img/due_dates_todos.png)
The day before an open issue is due, an email will be sent to all participants
of the issue. Both the due date and the day before are calculated using the
server's timezone.
[ce-3614]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3614
[permissions]: ../../permissions.md#project
......@@ -41,10 +41,7 @@ it's reassigned to someone else to take it from there.
if a user is not member of that project, it can only be
assigned to them if they created the issue themselves.
##### 3.1. Multiple Assignees
> Available in [GitLab Starter](https://about.gitlab.com/products/) and
[GitLab.com Bronze](https://about.gitlab.com/gitlab-com/).
##### 3.1. Multiple Assignees **[STARTER]**
Often multiple people likely work on the same issue together,
which can especially be difficult to track in large teams
......@@ -89,10 +86,7 @@ but they are immediately available to all projects in the group.
> **Tip:**
if the label doesn't exist yet, when you click **Edit**, it opens a dropdown menu from which you can select **Create new label**.
#### 8. Weight
> Available in [GitLab Starter](https://about.gitlab.com/products/) and
[GitLab.com Bronze](https://about.gitlab.com/gitlab-com/).
#### 8. Weight **[STARTER]**
- Attribute a weight (in a 0 to 9 range) to that issue. Easy to complete
should weight 1 and very hard to complete should weight 9.
......
......@@ -9,8 +9,7 @@ Labels allow you to categorize issues or merge requests using descriptive titles
In GitLab, you can create project and group labels:
- **Project labels** can be assigned to issues or merge requests in that project only.
- **Group labels** can be assigned to any issue or merge request of any project in that group or subgroup.
- In the [future](https://gitlab.com/gitlab-org/gitlab-ce/issues/40915), you will be able to assign group labels to issues and merge reqeusts of projects in [subgroups](../group/subgroups/index.md).
- **Group labels** can be assigned to any issue or merge request of any project in that group or any subgroups of the group.
## Creating labels
......
......@@ -32,10 +32,10 @@ With GitLab merge requests, you can:
With **[GitLab Enterprise Edition][ee]**, you can also:
- View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Premium)
- Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Starter)
- [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Starter)
- Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Starter)
- View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) **[PREMIUM]**
- Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers **[STARTER]**
- [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history **[STARTER]**
- Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) **[STARTER]**
## Use cases
......@@ -43,7 +43,7 @@ A. Consider you are a software developer working in a team:
1. You checkout a new branch, and submit your changes through a merge request
1. You gather feedback from your team
1. You work on the implementation optimizing code with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Starter)
1. You work on the implementation optimizing code with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) **[STARTER]**
1. You build and test your changes with GitLab CI/CD
1. You request the approval from your manager
1. Your manager pushes a commit with his final review, [approves the merge request](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html), and set it to [merge when pipeline succeeds](#merge-when-pipeline-succeeds) (Merge Request Approvals are available in GitLab Starter)
......@@ -56,7 +56,7 @@ B. Consider you're a web developer writing a webpage for your company's:
1. You gather feedback from your reviewers
1. Your changes are previewed with [Review Apps](../../../ci/review_apps/index.md)
1. You request your web designers for their implementation
1. You request the [approval](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your manager (available in GitLab Starter)
1. You request the [approval](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your manager **[STARTER]**
1. Once approved, your merge request is [squashed and merged](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html), and [deployed to staging with GitLab Pages](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) (Squash and Merge is available in GitLab Starter)
1. Your production team [cherry picks](#cherry-pick-changes) the merge commit into production
......
......@@ -34,7 +34,7 @@ Set up your project's merge request settings:
- Set up the merge request method (merge commit, [fast-forward merge](../merge_requests/fast_forward_merge.html)).
- Merge request [description templates](../description_templates.md#description-templates).
- Enable [merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#merge-request-approvals), _available in [GitLab Starter](https://about.gitlab.com/products/)_.
- Enable [merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#merge-request-approvals). **[STARTER]**
- Enable [merge only of pipeline succeeds](../merge_requests/merge_when_pipeline_succeeds.md).
- Enable [merge only when all discussions are resolved](../../discussions/index.md#only-allow-merge-requests-to-be-merged-if-all-discussions-are-resolved).
......
# Web IDE
> Introduced in [GitLab Ultimate][ee] 10.4.
> Brought to [GitLab CE][ce] in 10.7.
> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ee/issues/4539) [GitLab Ultimate][ee] 10.4.
> [Brought to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/issues/44157) in 10.7.
The Web IDE makes it faster and easier to contribute changes to your projects
by providing an advanced editor with commit staging.
......@@ -30,5 +30,4 @@ list.
An additional review mode is available when you open a merge request, which
shows you a preview of the merge request diff if you commit your changes.
[ee]: https://about.gitlab.com/products/
[ce]: https://about.gitlab.com/products/
[ee]: https://about.gitlab.com/pricing/
......@@ -86,6 +86,7 @@ In most of the below cases, the notification will be sent to:
| Close issue | |
| Reassign issue | The above, plus the old assignee |
| Reopen issue | |
| Due issue | Participants and Custom notification level with this event selected |
| New merge request | |
| Push to merge request | Participants and Custom notification level with this event selected |
| Reassign merge request | The above, plus the old assignee |
......@@ -96,15 +97,14 @@ In most of the below cases, the notification will be sent to:
| Failed pipeline | The author of the pipeline |
| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set |
In addition, if the title or description of an Issue or Merge Request is
changed, notifications will be sent to any **new** mentions by `@username` as
if they had been mentioned in the original text.
You won't receive notifications for Issues, Merge Requests or Milestones
created by yourself. You will only receive automatic notifications when
somebody else comments or adds changes to the ones that you've created or
mentions you.
You won't receive notifications for Issues, Merge Requests or Milestones created
by yourself (except when an issue is due). You will only receive automatic
notifications when somebody else comments or adds changes to the ones that
you've created or mentions you.
### Email Headers
......@@ -122,7 +122,7 @@ Notification emails include headers that provide extra content about the notific
| X-GitLab-NotificationReason | The reason for being notified. "mentioned", "assigned", etc |
#### X-GitLab-NotificationReason
This header holds the reason for the notification to have been sent out,
This header holds the reason for the notification to have been sent out,
where reason can be `mentioned`, `assigned`, `own_activity`, etc.
Only one reason is sent out according to its priority:
- `own_activity`
......@@ -130,7 +130,7 @@ Only one reason is sent out according to its priority:
- `mentioned`
The reason in this header will also be shown in the footer of the notification email. For example an email with the
reason `assigned` will have this sentence in the footer:
reason `assigned` will have this sentence in the footer:
`"You are receiving this email because you have been assigned an item on {configured GitLab hostname}"`
**Note: Only reasons listed above have been implemented so far**
......
Feature: Project Builds Permissions
Background:
Given I sign in as a user
And project exists in some group namespace
And project has CI enabled
And project has a recent build
Scenario: I try to visit build details as guest
Given I am member of a project with a guest role
When I visit recent build details page
Then page status code should be 404
Scenario: I try to visit project builds page as guest
Given I am member of a project with a guest role
When I visit project builds page
Then page status code should be 404
Scenario: I try to visit build details of internal project without access to builds
Given The project is internal
And public access for builds is disabled
When I visit recent build details page
Then page status code should be 404
Scenario: I try to visit internal project builds page without access to builds
Given The project is internal
And public access for builds is disabled
When I visit project builds page
Then page status code should be 404
@javascript
Scenario: I try to visit build details of internal project with access to builds
Given The project is internal
And public access for builds is enabled
When I visit recent build details page
Then I see details of a build
And I see build trace
Scenario: I try to visit internal project builds page with access to builds
Given The project is internal
And public access for builds is enabled
When I visit project builds page
Then I see the build
Scenario: I try to download build artifacts as guest
Given I am member of a project with a guest role
And recent build has artifacts available
When I access artifacts download page
Then page status code should be 404
Scenario: I try to download build artifacts as reporter
Given I am member of a project with a reporter role
And recent build has artifacts available
When I access artifacts download page
Then download of build artifacts archive starts
class Spinach::Features::ProjectBuildsPermissions < Spinach::FeatureSteps
include SharedAuthentication
include SharedProject
include SharedBuilds
include SharedPaths
include RepoHelpers
end
......@@ -30,10 +30,6 @@ module SharedBuilds
visit project_job_path(@project, @build)
end
step 'I visit project builds page' do
visit project_jobs_path(@project)
end
step 'recent build has artifacts available' do
artifacts = Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
archive = fixture_file_upload(artifacts, 'application/zip')
......@@ -54,25 +50,4 @@ module SharedBuilds
expect(page.response_headers['Content-Type']).to eq 'application/zip'
expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary'
end
step 'I access artifacts download page' do
visit download_project_job_artifacts_path(@project, @build)
end
step 'I see details of a build' do
expect(page).to have_content "Job ##{@build.id}"
end
step 'I see build trace' do
expect(page).to have_css '#build-trace'
end
step 'I see the build' do
page.within('.build') do
expect(page).to have_content "##{@build.id}"
expect(page).to have_content @build.sha[0..7]
expect(page).to have_content @build.ref
expect(page).to have_content @build.name
end
end
end
......@@ -435,12 +435,4 @@ module SharedPaths
mr = MergeRequest.find_by(title: title)
project_merge_request_path(mr.target_project, mr)
end
# ----------------------------------------
# Errors
# ----------------------------------------
step 'page status code should be 404' do
expect(status_code).to eq 404
end
end
......@@ -13,11 +13,6 @@ module SharedProject
@project.add_master(@user)
end
step "project exists in some group namespace" do
@group = create(:group, name: 'some group')
@project = create(:project, :repository, namespace: @group, public_builds: false)
end
# Create a specific project called "Shop"
step 'I own project "Shop"' do
@project = Project.find_by(name: "Shop")
......@@ -29,18 +24,6 @@ module SharedProject
@project ||= Project.first
end
# ----------------------------------------
# Project permissions
# ----------------------------------------
step 'I am member of a project with a guest role' do
@project.add_guest(@user)
end
step 'I am member of a project with a reporter role' do
@project.add_reporter(@user)
end
# ----------------------------------------
# Visibility of archived project
# ----------------------------------------
......@@ -140,18 +123,6 @@ module SharedProject
create(:label, project: project, title: 'enhancement')
end
step 'The project is internal' do
@project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
step 'public access for builds is enabled' do
@project.update(public_builds: true)
end
step 'public access for builds is disabled' do
@project.update(public_builds: false)
end
def user_owns_project(user_name:, project_name:, visibility: :private)
user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore)
project = Project.find_by(name: project_name)
......
......@@ -25,7 +25,7 @@ module API
get ":id/#{noteables_str}/:noteable_id/discussions" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
return not_found!("Discussions") unless can?(current_user, noteable_read_ability_name(noteable), noteable)
break not_found!("Discussions") unless can?(current_user, noteable_read_ability_name(noteable), noteable)
notes = noteable.notes
.inc_relations_for_view
......@@ -50,7 +50,7 @@ module API
notes = readable_discussion_notes(noteable, params[:discussion_id])
if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
return not_found!("Discussion")
break not_found!("Discussion")
end
discussion = Discussion.build(notes, noteable)
......@@ -98,7 +98,7 @@ module API
notes = readable_discussion_notes(noteable, params[:discussion_id])
if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
return not_found!("Notes")
break not_found!("Notes")
end
present notes, with: Entities::Note
......@@ -117,8 +117,8 @@ module API
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
return not_found!("Discussion") if notes.empty?
return bad_request!("Discussion is an individual note.") unless notes.first.part_of_discussion?
break not_found!("Discussion") if notes.empty?
break bad_request!("Discussion is an individual note.") unless notes.first.part_of_discussion?
opts = {
note: params[:body],
......
......@@ -31,7 +31,7 @@ module API
key = params[:key]
variable = user_group.variables.find_by(key: key)
return not_found!('GroupVariable') unless variable
break not_found!('GroupVariable') unless variable
present variable, with: Entities::Variable
end
......@@ -67,7 +67,7 @@ module API
put ':id/variables/:key' do
variable = user_group.variables.find_by(key: params[:key])
return not_found!('GroupVariable') unless variable
break not_found!('GroupVariable') unless variable
variable_params = declared_params(include_missing: false).except(:key)
......
......@@ -50,7 +50,7 @@ module API
access_checker.check(params[:action], params[:changes])
@project ||= access_checker.project
rescue Gitlab::GitAccess::UnauthorizedError, Gitlab::GitAccess::NotFoundError => e
return { status: false, message: e.message }
break { status: false, message: e.message }
end
log_user_activity(actor)
......@@ -142,21 +142,21 @@ module API
if key
key.update_last_used_at
else
return { 'success' => false, 'message' => 'Could not find the given key' }
break { 'success' => false, 'message' => 'Could not find the given key' }
end
if key.is_a?(DeployKey)
return { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
break { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
end
user = key.user
unless user
return { success: false, message: 'Could not find a user for the given key' }
break { success: false, message: 'Could not find a user for the given key' }
end
unless user.two_factor_enabled?
return { success: false, message: 'Two-factor authentication is not enabled for this user' }
break { success: false, message: 'Two-factor authentication is not enabled for this user' }
end
codes = nil
......
......@@ -310,7 +310,7 @@ module API
issue = find_project_issue(params[:issue_iid])
return not_found!('UserAgentDetail') unless issue.user_agent_detail
break not_found!('UserAgentDetail') unless issue.user_agent_detail
present issue.user_agent_detail, with: Entities::UserAgentDetail
end
......
......@@ -77,7 +77,7 @@ module API
build = find_build!(params[:job_id])
authorize!(:update_build, build)
return not_found!(build) unless build.artifacts?
break not_found!(build) unless build.artifacts?
build.keep_artifacts!
......
......@@ -120,7 +120,7 @@ module API
build = find_build!(params[:job_id])
authorize!(:update_build, build)
return forbidden!('Job is not retryable') unless build.retryable?
break forbidden!('Job is not retryable') unless build.retryable?
build = Ci::Build.retry(build, current_user)
......@@ -138,7 +138,7 @@ module API
build = find_build!(params[:job_id])
authorize!(:erase_build, build)
return forbidden!('Job is not erasable!') unless build.erasable?
break forbidden!('Job is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
present build, with: Entities::Job
......
......@@ -145,7 +145,7 @@ module API
snippet = Snippet.find_by!(id: params[:snippet_id], project_id: params[:id])
return not_found!('UserAgentDetail') unless snippet.user_agent_detail
break not_found!('UserAgentDetail') unless snippet.user_agent_detail
present snippet.user_agent_detail, with: Entities::UserAgentDetail
end
......
......@@ -402,7 +402,7 @@ module API
end
unless user_project.allowed_to_share_with_group?
return render_api_error!("The project sharing with group is disabled", 400)
break render_api_error!("The project sharing with group is disabled", 400)
end
link = user_project.project_group_links.new(declared_params(include_missing: false))
......
......@@ -111,8 +111,8 @@ module API
end
params do
use :pagination
optional :order_by, type: String, values: %w[email name commits], default: nil, desc: 'Return contributors ordered by `name` or `email` or `commits`'
optional :sort, type: String, values: %w[asc desc], default: nil, desc: 'Sort by asc (ascending) or desc (descending)'
optional :order_by, type: String, values: %w[email name commits], default: 'commits', desc: 'Return contributors ordered by `name` or `email` or `commits`'
optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
end
get ':id/repository/contributors' do
begin
......
......@@ -29,7 +29,7 @@ module API
project.runners.create(attributes)
end
return forbidden! unless runner
break forbidden! unless runner
if runner.id
present runner, with: Entities::RunnerRegistrationDetails
......@@ -83,7 +83,7 @@ module API
if current_runner.runner_queue_value_latest?(params[:last_update])
header 'X-GitLab-Last-Update', params[:last_update]
Gitlab::Metrics.add_event(:build_not_found_cached)
return no_content!
break no_content!
end
new_update = current_runner.ensure_runner_queue_value
......@@ -152,7 +152,7 @@ module API
stream_size = job.trace.append(request.body.read, content_range[0].to_i)
if stream_size < 0
return error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{-stream_size}" })
break error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{-stream_size}" })
end
status 202
......
......@@ -94,7 +94,7 @@ module API
end
put ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
break not_found!('Snippet') unless snippet
authorize! :update_personal_snippet, snippet
......@@ -120,7 +120,7 @@ module API
end
delete ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
break not_found!('Snippet') unless snippet
authorize! :destroy_personal_snippet, snippet
......@@ -135,7 +135,7 @@ module API
end
get ":id/raw" do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
break not_found!('Snippet') unless snippet
env['api.format'] = :txt
content_type 'text/plain'
......@@ -153,7 +153,7 @@ module API
snippet = Snippet.find_by!(id: params[:id])
return not_found!('UserAgentDetail') unless snippet.user_agent_detail
break not_found!('UserAgentDetail') unless snippet.user_agent_detail
present snippet.user_agent_detail, with: Entities::UserAgentDetail
end
......
......@@ -62,7 +62,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find(params.delete(:trigger_id))
return not_found!('Trigger') unless trigger
break not_found!('Trigger') unless trigger
present trigger, with: Entities::Trigger
end
......@@ -99,7 +99,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find(params.delete(:trigger_id))
return not_found!('Trigger') unless trigger
break not_found!('Trigger') unless trigger
if trigger.update(declared_params(include_missing: false))
present trigger, with: Entities::Trigger
......@@ -119,7 +119,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find(params.delete(:trigger_id))
return not_found!('Trigger') unless trigger
break not_found!('Trigger') unless trigger
if trigger.update(owner: current_user)
status :ok
......@@ -140,7 +140,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find(params.delete(:trigger_id))
return not_found!('Trigger') unless trigger
break not_found!('Trigger') unless trigger
destroy_conditionally!(trigger)
end
......
......@@ -51,7 +51,7 @@ module API
get ':id/repository/commits/:sha/builds' do
authorize_read_builds!
return not_found! unless user_project.commit(params[:sha])
break not_found! unless user_project.commit(params[:sha])
pipelines = user_project.pipelines.where(sha: params[:sha])
builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
......@@ -153,7 +153,7 @@ module API
build = get_build!(params[:build_id])
authorize!(:update_build, build)
return forbidden!('Build is not retryable') unless build.retryable?
break forbidden!('Build is not retryable') unless build.retryable?
build = Ci::Build.retry(build, current_user)
......@@ -171,7 +171,7 @@ module API
build = get_build!(params[:build_id])
authorize!(:erase_build, build)
return forbidden!('Build is not erasable!') unless build.erasable?
break forbidden!('Build is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
present build, with: ::API::V3::Entities::Build
......@@ -188,7 +188,7 @@ module API
build = get_build!(params[:build_id])
authorize!(:update_build, build)
return not_found!(build) unless build.artifacts?
break not_found!(build) unless build.artifacts?
build.keep_artifacts!
......
......@@ -423,7 +423,7 @@ module API
end
unless user_project.allowed_to_share_with_group?
return render_api_error!("The project sharing with group is disabled", 400)
break render_api_error!("The project sharing with group is disabled", 400)
end
link = user_project.project_group_links.new(declared_params(include_missing: false))
......
......@@ -90,7 +90,7 @@ module API
end
put ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
break not_found!('Snippet') unless snippet
authorize! :update_personal_snippet, snippet
......@@ -114,7 +114,7 @@ module API
end
delete ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
break not_found!('Snippet') unless snippet
authorize! :destroy_personal_snippet, snippet
snippet.destroy
......@@ -129,7 +129,7 @@ module API
end
get ":id/raw" do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
break not_found!('Snippet') unless snippet
env['api.format'] = :txt
content_type 'text/plain'
......
......@@ -72,7 +72,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find_by(token: params[:token].to_s)
return not_found!('Trigger') unless trigger
break not_found!('Trigger') unless trigger
present trigger, with: ::API::V3::Entities::Trigger
end
......@@ -100,7 +100,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find_by(token: params[:token].to_s)
return not_found!('Trigger') unless trigger
break not_found!('Trigger') unless trigger
trigger.destroy
......
......@@ -31,7 +31,7 @@ module API
key = params[:key]
variable = user_project.variables.find_by(key: key)
return not_found!('Variable') unless variable
break not_found!('Variable') unless variable
present variable, with: Entities::Variable
end
......@@ -67,7 +67,7 @@ module API
put ':id/variables/:key' do
variable = user_project.variables.find_by(key: params[:key])
return not_found!('Variable') unless variable
break not_found!('Variable') unless variable
variable_params = declared_params(include_missing: false).except(:key)
......
......@@ -9,7 +9,7 @@ module Banzai
lang_attr = lang.present? ? %Q{ lang="#{lang}"} : ''
result =
"<pre>" \
"<code#{lang_attr}>#{html_escape(code)}</code>" \
"<code#{lang_attr}>#{ERB::Util.html_escape(code)}</code>" \
"</pre>"
out(result)
......
......@@ -6,7 +6,7 @@ module Banzai
lang_attr = lang ? %Q{ lang="#{lang}"} : ''
"\n<pre>" \
"<code#{lang_attr}>#{html_escape(code)}</code>" \
"<code#{lang_attr}>#{ERB::Util.html_escape(code)}</code>" \
"</pre>"
end
end
......
......@@ -77,7 +77,7 @@ module DeclarativePolicy
@state = State.new
steps_by_score do |step, score|
return if !debug && @state.prevented?
break if !debug && @state.prevented?
passed = nil
case step.action
......
......@@ -10,11 +10,15 @@ module Gitlab
Gitlab.config.gitlab.url == COM_URL || gl_subdomain?
end
def self.org?
Gitlab.config.gitlab.url == 'https://dev.gitlab.org'
end
def self.gl_subdomain?
SUBDOMAIN_REGEX === Gitlab.config.gitlab.url
end
def self.dev_env_or_com?
Rails.env.test? || Rails.env.development? || com?
Rails.env.test? || Rails.env.development? || org? || com?
end
end
......@@ -51,7 +51,7 @@ module Gitlab
Gitlab::Auth::UniqueIpsLimiter.limit_user! do
user = User.by_login(login)
return if user && !user.active?
break if user && !user.active?
authenticators = []
......
......@@ -45,7 +45,7 @@ module Gitlab
def append(data, offset)
write do |stream|
current_length = stream.size
return -current_length unless current_length == offset
break -current_length unless current_length == offset
data = job.hide_secrets(data)
stream.append(data, offset)
......
......@@ -75,18 +75,28 @@ module Gitlab
end
end
def read(length = nil)
def read(length = nil, outbuf = "")
out = ""
until eof? || (length && out.length >= length)
length ||= size - tell
until length <= 0 || eof?
data = get_chunk
break if data.empty?
out << data
@tell += data.bytesize
chunk_bytes = [BUFFER_SIZE - chunk_offset, length].min
chunk_data = data.byteslice(0, chunk_bytes)
out << chunk_data
@tell += chunk_data.bytesize
length -= chunk_data.bytesize
end
out = out[0, length] if length && out.length > length
# If outbuf is passed, we put the output into the buffer. This supports IO.copy_stream functionality
if outbuf
outbuf.slice!(0, outbuf.bytesize)
outbuf << out
end
out
end
......@@ -158,7 +168,7 @@ module Gitlab
# Provider: GCS
# - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
# - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPOK 200
@chunk_range ||= (chunk_start...(chunk_start + @chunk.length))
@chunk_range ||= (chunk_start...(chunk_start + @chunk.bytesize))
end
@chunk[chunk_offset..BUFFER_SIZE]
......
......@@ -10,7 +10,9 @@ module Gitlab
delegate :close, :tell, :seek, :size, :url, :truncate, to: :stream, allow_nil: true
delegate :valid?, to: :stream, as: :present?, allow_nil: true
delegate :valid?, to: :stream, allow_nil: true
alias_method :present?, :valid?
def initialize
@stream = yield
......@@ -85,7 +87,7 @@ module Gitlab
match = matches.flatten.last
coverage = match.gsub(/\d+(\.\d+)?/).first
return coverage if coverage.present?
return coverage if coverage.present? # rubocop:disable Cop/AvoidReturnFromBlocks
end
nil
......
......@@ -30,7 +30,7 @@ module Gitlab
return unless enabled?
@mutex.synchronize do
return thread if thread?
break thread if thread?
@thread = Thread.new { start_working }
end
......@@ -38,7 +38,7 @@ module Gitlab
def stop
@mutex.synchronize do
return unless thread?
break unless thread?
stop_working
......
......@@ -33,10 +33,7 @@ module Gitlab
# match the blob, which is a bug. But we shouldn't fail to render
# completely in that case, even though we want to report the error.
rescue RangeError => e
if Gitlab::Sentry.enabled?
Gitlab::Sentry.context
Raven.capture_exception(e)
end
Gitlab::Sentry.track_exception(e, issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/45441')
end
end
......
......@@ -5,7 +5,7 @@ module Gitlab
CANONICAL_CE_PROJECT_URL = 'https://gitlab.com/gitlab-org/gitlab-ce'.freeze
CANONICAL_EE_REPO_URL = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
CHECK_DIR = Rails.root.join('ee_compat_check')
IGNORED_FILES_REGEX = %r{VERSION|CHANGELOG\.md|db/schema\.rb}i.freeze
IGNORED_FILES_REGEX = %r{VERSION|CHANGELOG\.md|db/schema\.rb|locale/gitlab\.pot}i.freeze
PLEASE_READ_THIS_BANNER = %Q{
============================================================
===================== PLEASE READ THIS =====================
......
......@@ -50,7 +50,7 @@ module Gitlab
status_code = Gitlab::PollingInterval.polling_enabled? ? 304 : 429
[status_code, { 'ETag' => etag }, []]
[status_code, { 'ETag' => etag, 'X-Gitlab-From-Cache' => 'true' }, []]
end
def track_cache_miss(if_none_match, cached_value_present, route)
......
......@@ -21,7 +21,7 @@ module Gitlab
@text.gsub(@pattern) do |markdown|
file = find_file(@source_project, $~[:secret], $~[:file])
return markdown unless file.try(:exists?)
break markdown unless file.try(:exists?)
new_uploader = FileUploader.new(target_project)
with_link_in_tmp_dir(file.file) do |open_tmp_file|
......
......@@ -486,6 +486,8 @@ module Gitlab
end
def tree_entry(path)
return unless path.present?
@repository.gitaly_migrate(:commit_tree_entry) do |is_migrated|
if is_migrated
gitaly_tree_entry(path)
......
......@@ -249,7 +249,7 @@ module Gitlab
if size >= SIZE_LIMIT
too_large!
return true
return true # rubocop:disable Cop/AvoidReturnFromBlocks
end
end
end
......
......@@ -25,7 +25,9 @@ module Gitlab
stdin.close
if lazy_block
return [lazy_block.call(stdout.lazy), 0]
cmd_output = lazy_block.call(stdout.lazy)
cmd_status = 0
break
else
cmd_output << stdout.read
end
......
......@@ -9,6 +9,7 @@ module Gitlab
include Gitlab::Git::RepositoryMirroring
include Gitlab::Git::Popen
include Gitlab::EncodingHelper
include Gitlab::Utils::StrongMemoize
ALLOWED_OBJECT_DIRECTORIES_VARIABLES = %w[
GIT_OBJECT_DIRECTORY
......@@ -231,13 +232,13 @@ module Gitlab
end
end
def expire_has_local_branches_cache
clear_memoization(:has_local_branches)
end
def has_local_branches?
gitaly_migrate(:has_local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_repository_client.has_local_branches?
else
has_local_branches_rugged?
end
strong_memoize(:has_local_branches) do
uncached_has_local_branches?
end
end
......@@ -299,7 +300,8 @@ module Gitlab
#
# Ref names must start with `refs/`.
def ref_exists?(ref_name)
gitaly_migrate(:ref_exists) do |is_enabled|
gitaly_migrate(:ref_exists,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_ref_exists?(ref_name)
else
......@@ -1559,6 +1561,16 @@ module Gitlab
private
def uncached_has_local_branches?
gitaly_migrate(:has_local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_repository_client.has_local_branches?
else
has_local_branches_rugged?
end
end
end
def local_write_ref(ref_path, ref, old_ref: nil, shell: true)
if shell
shell_write_ref(ref_path, ref, old_ref)
......
......@@ -26,7 +26,7 @@ module Gitlab
# When the remote repo does not have tags.
if target.nil? || path.nil?
Rails.logger.info "Empty or invalid list of tags for remote: #{remote}. Output: #{output}"
return []
break []
end
name = path.split('/', 3).last
......
......@@ -3,18 +3,15 @@ module Gitlab
module_function
def retry_lock(subject, retries = 100, &block)
loop do
begin
ActiveRecord::Base.transaction do
return yield(subject)
end
rescue ActiveRecord::StaleObjectError
retries -= 1
raise unless retries >= 0
subject.reload
end
ActiveRecord::Base.transaction do
yield(subject)
end
rescue ActiveRecord::StaleObjectError
retries -= 1
raise unless retries >= 0
subject.reload
retry
end
alias_method :retry_optimistic_lock, :retry_lock
......
......@@ -18,6 +18,25 @@ module Gitlab
end
end
# This can be used for investigating exceptions that can be recovered from in
# code. The exception will still be raised in development and test
# environments.
#
# That way we can track down these exceptions with as much information as we
# need to resolve them.
#
# Provide an issue URL for follow up.
def self.track_exception(exception, issue_url: nil, extra: {})
if enabled?
extra[:issue_url] = issue_url if issue_url
context # Make sure we've set everything we know in the context
Raven.capture_exception(exception, extra: extra)
end
raise exception if should_raise?
end
def self.program_context
if Sidekiq.server?
'sidekiq'
......@@ -25,5 +44,9 @@ module Gitlab
'rails'
end
end
def self.should_raise?
Rails.env.development? || Rails.env.test?
end
end
end
......@@ -340,7 +340,7 @@ module Gitlab
if enabled
gitaly_namespace_client(storage).rename(old_name, new_name)
else
return false if exists?(storage, new_name) || !exists?(storage, old_name)
break false if exists?(storage, new_name) || !exists?(storage, old_name)
FileUtils.mv(full_path(storage, old_name), full_path(storage, new_name))
end
......
......@@ -25,7 +25,7 @@ module Gitlab
# can be only one shutdown thread in the process.
def self.create_shutdown_thread
mu_synchronize do
return unless @shutdown_thread.nil?
break unless @shutdown_thread.nil?
@shutdown_thread = Thread.new { yield }
end
......
......@@ -111,7 +111,7 @@ namespace :gitlab do
puts " - #{project.full_path} (id: #{project.id})".color(:red)
return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator
return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator, Cop/AvoidReturnFromBlocks
end
end
end
......@@ -132,7 +132,7 @@ namespace :gitlab do
puts " - #{upload.path} (id: #{upload.id})".color(:red)
return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator
return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator, Cop/AvoidReturnFromBlocks
end
end
end
......
......@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-04 18:02+0200\n"
"PO-Revision-Date: 2018-04-04 18:02+0200\n"
"POT-Creation-Date: 2018-04-17 11:44+0200\n"
"PO-Revision-Date: 2018-04-17 11:44+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
......@@ -241,6 +241,9 @@ msgstr ""
msgid "Allow edits from maintainers."
msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
msgstr ""
msgid "Allow requests to the local network from hooks and services."
msgstr ""
......@@ -322,7 +325,7 @@ msgstr ""
msgid "April"
msgstr ""
msgid "Archived project! Repository is read-only"
msgid "Archived project! Repository and other project resources are read-only"
msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
......@@ -433,9 +436,81 @@ msgstr ""
msgid "Background jobs"
msgstr ""
msgid "Badges"
msgstr ""
msgid "Badges|A new badge was added."
msgstr ""
msgid "Badges|Add badge"
msgstr ""
msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
msgstr ""
msgid "Badges|Badge image URL"
msgstr ""
msgid "Badges|Badge image preview"
msgstr ""
msgid "Badges|Delete badge"
msgstr ""
msgid "Badges|Delete badge?"
msgstr ""
msgid "Badges|Deleting the badge failed, please try again."
msgstr ""
msgid "Badges|Group Badge"
msgstr ""
msgid "Badges|Link"
msgstr ""
msgid "Badges|No badge image"
msgstr ""
msgid "Badges|No image to preview"
msgstr ""
msgid "Badges|Project Badge"
msgstr ""
msgid "Badges|Reload badge image"
msgstr ""
msgid "Badges|Save changes"
msgstr ""
msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
msgstr ""
msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
msgstr ""
msgid "Badges|The badge was deleted."
msgstr ""
msgid "Badges|The badge was saved."
msgstr ""
msgid "Badges|This group has no badges"
msgstr ""
msgid "Badges|This project has no badges"
msgstr ""
msgid "Badges|Your badges"
msgstr ""
msgid "Begin with the selected commit"
msgstr ""
msgid "Blame"
msgstr ""
msgid "Branch (%{branch_count})"
msgid_plural "Branches (%{branch_count})"
msgstr[0] ""
......@@ -600,12 +675,18 @@ msgstr ""
msgid "Cancel"
msgstr ""
msgid "Cancel this job"
msgstr ""
msgid "Cannot be merged automatically"
msgstr ""
msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
msgstr ""
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr ""
......@@ -744,6 +825,12 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
......@@ -816,7 +903,7 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
msgid "ClusterIntegration|Create on GKE"
msgid "ClusterIntegration|Create on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
......@@ -1147,6 +1234,9 @@ msgstr ""
msgid "Confidentiality"
msgstr ""
msgid "Configure Gitaly timeouts."
msgstr ""
msgid "Configure Sidekiq job throttling."
msgstr ""
......@@ -1213,6 +1303,9 @@ msgstr ""
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
msgstr ""
msgid "ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images."
msgstr ""
msgid "Continuous Integration and Deployment"
msgstr ""
......@@ -1386,6 +1479,78 @@ msgstr[1] ""
msgid "Deploy Keys"
msgstr ""
msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
msgstr ""
msgid "DeployTokens|Add a deploy token"
msgstr ""
msgid "DeployTokens|Allows read-only access to the registry images"
msgstr ""
msgid "DeployTokens|Allows read-only access to the repository"
msgstr ""
msgid "DeployTokens|Copy deploy token to clipboard"
msgstr ""
msgid "DeployTokens|Copy username to clipboard"
msgstr ""
msgid "DeployTokens|Create deploy token"
msgstr ""
msgid "DeployTokens|Created"
msgstr ""
msgid "DeployTokens|Deploy Tokens"
msgstr ""
msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
msgstr ""
msgid "DeployTokens|Expires"
msgstr ""
msgid "DeployTokens|Name"
msgstr ""
msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
msgstr ""
msgid "DeployTokens|Revoke"
msgstr ""
msgid "DeployTokens|Revoke %{name}"
msgstr ""
msgid "DeployTokens|Scopes"
msgstr ""
msgid "DeployTokens|This action cannot be undone."
msgstr ""
msgid "DeployTokens|This project has no active Deploy Tokens."
msgstr ""
msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
msgstr ""
msgid "DeployTokens|Use this username as a login."
msgstr ""
msgid "DeployTokens|Username"
msgstr ""
msgid "DeployTokens|You are about to revoke"
msgstr ""
msgid "DeployTokens|Your New Deploy Token"
msgstr ""
msgid "DeployTokens|Your new project deploy token has been created."
msgstr ""
msgid "Description"
msgstr ""
......@@ -1455,9 +1620,15 @@ msgstr ""
msgid "Editing"
msgstr ""
msgid "Email"
msgstr ""
msgid "Emails"
msgstr ""
msgid "Embed"
msgstr ""
msgid "Enable Auto DevOps"
msgstr ""
......@@ -1470,6 +1641,9 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
msgid "Enable or disable version check and usage ping."
msgstr ""
msgid "Enable reCAPTCHA or Akismet and set IP limits."
msgstr ""
......@@ -1703,6 +1877,9 @@ msgstr ""
msgid "GitLab Runner section"
msgstr ""
msgid "Gitaly"
msgstr ""
msgid "Gitaly Servers"
msgstr ""
......@@ -1885,6 +2062,9 @@ msgstr ""
msgid "January"
msgstr ""
msgid "Job has been erased"
msgstr ""
msgid "Jobs"
msgstr ""
......@@ -1900,6 +2080,9 @@ msgstr ""
msgid "June"
msgstr ""
msgid "Koding"
msgstr ""
msgid "Kubernetes"
msgstr ""
......@@ -2321,6 +2504,9 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr ""
msgid "Online IDE integration settings."
msgstr ""
msgid "Only project members can comment."
msgstr ""
......@@ -2369,6 +2555,12 @@ msgstr ""
msgid "Pending"
msgstr ""
msgid "Performance optimization"
msgstr ""
msgid "Permalink"
msgstr ""
msgid "Personal Access Token"
msgstr ""
......@@ -2510,12 +2702,18 @@ msgstr ""
msgid "Pipeline|with stages"
msgstr ""
msgid "PlantUML"
msgstr ""
msgid "Play"
msgstr ""
msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
msgstr ""
msgid "Please select at least one filter to see results"
msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
......@@ -2540,6 +2738,12 @@ msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
msgid "Profiles|Change username"
msgstr ""
msgid "Profiles|Current path: %{path}"
msgstr ""
msgid "Profiles|Delete Account"
msgstr ""
......@@ -2558,9 +2762,21 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
msgid "Profiles|Path"
msgstr ""
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
msgid "Profiles|Update username"
msgstr ""
msgid "Profiles|Username change failed - %{message}"
msgstr ""
msgid "Profiles|Username successfully changed"
msgstr ""
msgid "Profiles|You don't have access to delete this user."
msgstr ""
......@@ -2591,6 +2807,9 @@ msgstr ""
msgid "Project '%{project_name}' was successfully updated."
msgstr ""
msgid "Project Badges"
msgstr ""
msgid "Project access must be granted explicitly to each user."
msgstr ""
......@@ -2618,15 +2837,6 @@ msgstr ""
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
msgid "ProjectFeature|Disabled"
msgstr ""
msgid "ProjectFeature|Everyone with access"
msgstr ""
msgid "ProjectFeature|Only team members"
msgstr ""
msgid "ProjectFileTree|Name"
msgstr ""
......@@ -2663,6 +2873,9 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
msgid "PrometheusDashboard|Time"
msgstr ""
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
......@@ -2729,6 +2942,9 @@ msgstr ""
msgid "Promote"
msgstr ""
msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to Group Label"
msgstr ""
......@@ -2756,12 +2972,18 @@ msgstr ""
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
msgid "Raw"
msgstr ""
msgid "Read more"
msgstr ""
msgid "Readme"
msgstr ""
msgid "Real-time features"
msgstr ""
msgid "RefSwitcher|Branches"
msgstr ""
......@@ -2828,6 +3050,12 @@ msgstr ""
msgid "Resolve discussion"
msgstr ""
msgid "Retry this job"
msgstr ""
msgid "Retry verification"
msgstr ""
msgid "Reveal value"
msgid_plural "Reveal values"
msgstr[0] ""
......@@ -2839,6 +3067,9 @@ msgstr ""
msgid "Revert this merge request"
msgstr ""
msgid "Review"
msgstr ""
msgid "Reviewing"
msgstr ""
......@@ -2941,6 +3172,9 @@ msgstr ""
msgid "Set default and restrict visibility levels. Configure import sources and git access protocol."
msgstr ""
msgid "Set max session time for web terminal."
msgstr ""
msgid "Set notification email for abuse reports."
msgstr ""
......@@ -2962,6 +3196,9 @@ msgstr ""
msgid "Setup a specific Runner automatically"
msgstr ""
msgid "Share"
msgstr ""
msgid "Show command"
msgstr ""
......@@ -3141,6 +3378,9 @@ msgstr ""
msgid "Status"
msgstr ""
msgid "Stop this environment"
msgstr ""
msgid "Stopped"
msgstr ""
......@@ -3371,6 +3611,15 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
msgid "This job does not have a trace."
msgstr ""
msgid "This job has been canceled"
msgstr ""
msgid "This job has been skipped"
msgstr ""
msgid "This job has not been triggered yet"
msgstr ""
......@@ -3392,6 +3641,9 @@ msgstr ""
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
msgid "This page will be removed in a future release."
msgstr ""
msgid "This project"
msgstr ""
......@@ -3615,6 +3867,9 @@ msgstr ""
msgid "Unstar"
msgstr ""
msgid "Unverified"
msgstr ""
msgid "Up to date"
msgstr ""
......@@ -3633,6 +3888,12 @@ msgstr ""
msgid "Upvotes"
msgstr ""
msgid "Usage statistics"
msgstr ""
msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgid "Use the following registration token during setup:"
msgstr ""
......@@ -3645,6 +3906,18 @@ msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
msgstr ""
msgid "Various container registry settings."
msgstr ""
msgid "Various email settings."
msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
msgid "Verified"
msgstr ""
msgid "View and edit lines"
msgstr ""
......@@ -3696,6 +3969,9 @@ msgstr ""
msgid "Web IDE"
msgstr ""
msgid "Web terminal"
msgstr ""
msgid "Wiki"
msgstr ""
......@@ -3938,6 +4214,9 @@ msgid_plural "days"
msgstr[0] ""
msgstr[1] ""
msgid "deploy token"
msgstr ""
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
......@@ -3988,6 +4267,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
msgid "mrWidget|Create an issue to resolve them later"
msgstr ""
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
......@@ -4081,6 +4363,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
msgstr ""
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
......
......@@ -31,6 +31,7 @@ module QA
autoload :Project, 'qa/factory/resource/project'
autoload :MergeRequest, 'qa/factory/resource/merge_request'
autoload :DeployKey, 'qa/factory/resource/deploy_key'
autoload :Branch, 'qa/factory/resource/branch'
autoload :SecretVariable, 'qa/factory/resource/secret_variable'
autoload :Runner, 'qa/factory/resource/runner'
autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token'
......@@ -132,6 +133,7 @@ module QA
autoload :Repository, 'qa/page/project/settings/repository'
autoload :CICD, 'qa/page/project/settings/ci_cd'
autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
autoload :ProtectedBranches, 'qa/page/project/settings/protected_branches'
autoload :SecretVariables, 'qa/page/project/settings/secret_variables'
autoload :Runners, 'qa/page/project/settings/runners'
autoload :MergeRequest, 'qa/page/project/settings/merge_request'
......
......@@ -22,7 +22,7 @@ module QA
factory.fabricate!(*args)
return Factory::Product.populate!(factory)
break Factory::Product.populate!(factory)
end
end
......
module QA
module Factory
module Resource
class Branch < Factory::Base
attr_accessor :project, :branch_name, :allow_to_push, :protected
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'protected-branch-project'
end
product :name do
Page::Project::Settings::Repository.act do
expand_protected_branches(&:last_branch_name)
end
end
product :push_allowance do
Page::Project::Settings::Repository.act do
expand_protected_branches(&:last_push_allowance)
end
end
def initialize
@branch_name = 'test/branch'
@allow_to_push = true
@protected = false
end
def fabricate!
project.visit!
Factory::Repository::Push.fabricate! do |resource|
resource.project = project
resource.file_name = 'kick-off.txt'
resource.commit_message = 'First commit'
end
branch = Factory::Repository::Push.fabricate! do |resource|
resource.project = project
resource.file_name = 'README.md'
resource.commit_message = 'Add readme'
resource.branch_name = "master:#{@branch_name}"
end
Page::Project::Show.act { wait_for_push }
# The upcoming process will make it access the Protected Branches page,
# select the already created branch and protect it according
# to `allow_to_push` variable.
return branch unless @protected
Page::Menu::Side.act do
click_repository_settings
end
Page::Project::Settings::Repository.perform do |setting|
setting.expand_protected_branches do |page|
page.select_branch(branch_name)
if allow_to_push
page.allow_devs_and_masters_to_push
else
page.allow_no_one_to_push
end
page.protect_branch
end
end
end
end
end
end
end
require 'cgi'
require 'uri'
require 'open3'
module QA
module Git
class Repository
include Scenario::Actable
attr_reader :push_error
def self.perform(*args)
Dir.mktmpdir do |dir|
Dir.chdir(dir) { super }
......@@ -65,7 +68,8 @@ module QA
end
def push_changes(branch = 'master')
`git push #{@uri.to_s} #{branch} #{suppress_output}`
# capture3 returns stdout, stderr and status.
_, @push_error, _ = Open3.capture3("git push #{@uri} #{branch} #{suppress_output}")
end
def commits
......
......@@ -29,7 +29,7 @@ module QA
filter_by_name(name)
wait(reload: false) do
return false if page.has_content?('Sorry, no groups or projects matched your search')
break false if page.has_content?('Sorry, no groups or projects matched your search')
page.has_link?(name)
end
......
......@@ -20,14 +20,14 @@ module QA::Page
def running?
within('.ci-header-container') do
return page.has_content?('running')
page.has_content?('running')
end
end
def has_build?(name, status: :success)
within('.pipeline-graph') do
within('.ci-job-component', text: name) do
return has_selector?(".ci-status-icon-#{status}")
has_selector?(".ci-status-icon-#{status}")
end
end
end
......
module QA
module Page
module Project
module Settings
class ProtectedBranches < Page::Base
view 'app/views/projects/protected_branches/shared/_dropdown.html.haml' do
element :protected_branch_select
element :protected_branch_dropdown
end
view 'app/views/projects/protected_branches/_create_protected_branch.html.haml' do
element :allowed_to_push_select
element :allowed_to_push_dropdown
end
view 'app/views/projects/protected_branches/shared/_branches_list.html.haml' do
element :protected_branches_list
end
view 'app/views/projects/protected_branches/shared/_protected_branch.html.haml' do
element :protected_branch_name
end
def select_branch(branch_name)
click_element :protected_branch_select
within_element(:protected_branch_dropdown) do
click_on branch_name
end
end
def allow_no_one_to_push
allow_to_push('No one')
end
def allow_devs_and_masters_to_push
allow_to_push('Developers + Masters')
end
def protect_branch
click_on 'Protect'
end
def last_branch_name
within_element(:protected_branches_list) do
all('.qa-protected-branch-name').last
end
end
def last_push_allowance
within_element(:protected_branches_list) do
all('.qa-allowed-to-push').last
end
end
private
def allow_to_push(text)
click_element :allowed_to_push_select
within_element(:allowed_to_push_dropdown) do
click_on text
end
end
end
end
end
end
end
......@@ -14,6 +14,12 @@ module QA
DeployKeys.perform(&block)
end
end
def expand_protected_branches(&block)
expand_section('Protected Branches') do
ProtectedBranches.perform(&block)
end
end
end
end
end
......
......@@ -21,6 +21,11 @@ module QA
element :new_issue_link, "link_to 'New issue', new_project_issue_path(@project)"
end
view 'app/views/shared/_ref_switcher.html.haml' do
element :branches_select
element :branches_dropdown
end
def choose_repository_clone_http
choose_repository_clone('HTTP', 'http')
end
......@@ -44,6 +49,18 @@ module QA
find('.qa-project-name').text
end
def switch_to_branch(branch_name)
find_element(:branches_select).click
within_element(:branches_dropdown) do
click_on branch_name
end
end
def last_commit_content
find_element(:commit_content).text
end
def new_merge_request
wait(reload: true) do
has_css?(element_selector_css(:create_merge_request))
......
......@@ -4,7 +4,7 @@ module QA
def self.perform(*args)
new.tap do |scenario|
yield scenario if block_given?
return scenario.perform(*args)
break scenario.perform(*args)
end
end
......
module QA
feature 'branch protection support', :core do
given(:branch_name) { 'protected-branch' }
given(:commit_message) { 'Protected push commit message' }
given(:project) do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'protected-branch-project'
end
end
given(:location) do
Page::Project::Show.act do
choose_repository_clone_http
repository_location
end
end
before do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
end
scenario 'user is able to protect a branch' do
protected_branch = Factory::Resource::Branch.fabricate! do |resource|
resource.branch_name = branch_name
resource.project = project
resource.allow_to_push = true
resource.protected = true
end
expect(protected_branch.name).to have_content(branch_name)
expect(protected_branch.push_allowance).to have_content('Developers + Masters')
end
scenario 'users without authorization cannot push to protected branch' do
Factory::Resource::Branch.fabricate! do |resource|
resource.branch_name = branch_name
resource.project = project
resource.allow_to_push = false
resource.protected = true
end
project.visit!
Git::Repository.perform do |repository|
repository.location = location
repository.use_default_credentials
repository.act do
clone
configure_identity('GitLab QA', 'root@gitlab.com')
checkout('protected-branch')
commit_file('README.md', 'readme content', 'Add a readme')
push_changes('protected-branch')
end
expect(repository.push_error)
.to match(/remote\: GitLab\: You are not allowed to push code to protected branches on this project/)
expect(repository.push_error)
.to match(/\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/)
end
end
end
end
# frozen_string_literal: true
module RuboCop
module Cop
# Checks for break inside strong_memoize blocks.
# For more information see: https://gitlab.com/gitlab-org/gitlab-ce/issues/42889
#
# @example
# # bad
# strong_memoize(:result) do
# break if something
#
# do_an_heavy_calculation
# end
#
# # good
# strong_memoize(:result) do
# next if something
#
# do_an_heavy_calculation
# end
#
class AvoidBreakFromStrongMemoize < RuboCop::Cop::Cop
MSG = 'Do not use break inside strong_memoize, use next instead.'
def on_block(node)
block_body = node.body
return unless block_body
return unless node.method_name == :strong_memoize
block_body.each_node(:break) do |break_node|
next if container_block_for(break_node) != node
add_offense(break_node)
end
end
private
def container_block_for(current_node)
current_node = current_node.parent until current_node.type == :block && current_node.method_name == :strong_memoize
current_node
end
end
end
end
# frozen_string_literal: true
module RuboCop
module Cop
# Checks for return inside blocks.
# For more information see: https://gitlab.com/gitlab-org/gitlab-ce/issues/42889
#
# @example
# # bad
# call do
# return if something
#
# do_something_else
# end
#
# # good
# call do
# break if something
#
# do_something_else
# end
#
class AvoidReturnFromBlocks < RuboCop::Cop::Cop
MSG = 'Do not return from a block, use next or break instead.'
DEF_METHODS = %i[define_method lambda].freeze
WHITELISTED_METHODS = %i[each each_filename times loop].freeze
def on_block(node)
block_body = node.body
return unless block_body
return unless top_block?(node)
block_body.each_node(:return) do |return_node|
next if parent_blocks(node, return_node).all?(&method(:whitelisted?))
add_offense(return_node)
end
end
private
def top_block?(node)
current_node = node
top_block = nil
while current_node && current_node.type != :def
top_block = current_node if current_node.type == :block
current_node = current_node.parent
end
top_block == node
end
def parent_blocks(node, current_node)
blocks = []
until node == current_node || def?(current_node)
blocks << current_node if current_node.type == :block
current_node = current_node.parent
end
blocks << node if node == current_node && !def?(node)
blocks
end
def def?(node)
node.type == :def || node.type == :defs ||
(node.type == :block && DEF_METHODS.include?(node.method_name))
end
def whitelisted?(block_node)
WHITELISTED_METHODS.include?(block_node.method_name)
end
end
end
end
......@@ -61,7 +61,7 @@ module RuboCop
return true unless opts
each_hash_node_pair(opts) do |key, value|
return value == 'nil' if key == :default
break value == 'nil' if key == :default
end
end
......@@ -69,7 +69,7 @@ module RuboCop
return true unless opts
each_hash_node_pair(opts) do |key, value|
return value != 'false' if key == :null
break value != 'false' if key == :null
end
end
......
......@@ -4,6 +4,8 @@ require_relative 'cop/gitlab/httparty'
require_relative 'cop/gitlab/module_with_instance_variables'
require_relative 'cop/gitlab/predicate_memoization'
require_relative 'cop/include_sidekiq_worker'
require_relative 'cop/avoid_return_from_blocks'
require_relative 'cop/avoid_break_from_strong_memoize'
require_relative 'cop/line_break_around_conditional_block'
require_relative 'cop/migration/add_column'
require_relative 'cop/migration/add_concurrent_foreign_key'
......
......@@ -62,6 +62,7 @@ FactoryBot.define do
end
trait :pending do
queued_at 'Di 29. Okt 09:50:59 CET 2013'
status 'pending'
end
......
require 'spec_helper'
describe 'IDE', :js do
describe 'sub-groups' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) }
let(:subgroup_project) { create(:project, :repository, namespace: subgroup) }
before do
subgroup_project.add_master(user)
sign_in(user)
visit project_path(subgroup_project)
click_link('Web IDE')
wait_for_requests
end
it 'loads project in web IDE' do
expect(page).to have_selector('.context-header', text: subgroup_project.name)
end
end
end
......@@ -27,6 +27,23 @@ describe 'Merge request > User scrolls to note on load', :js do
expect(fragment_position_top).to be < (page_scroll_y + page_height)
end
it 'renders un-collapsed notes with diff' do
page.current_window.resize_to(1000, 1000)
visit "#{project_merge_request_path(project, merge_request)}#{fragment_id}"
page.execute_script "window.scrollTo(0,0)"
note_element = find(fragment_id)
note_container = note_element.ancestor('.js-toggle-container')
expect(note_element.visible?).to eq true
page.within note_container do
expect(page).not_to have_selector('.js-error-lazy-load-diff')
end
end
it 'expands collapsed notes' do
visit "#{project_merge_request_path(project, merge_request)}#{collapsed_fragment_id}"
note_element = find(collapsed_fragment_id)
......
......@@ -33,7 +33,7 @@ feature 'Gcp Cluster', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
click_link 'Create on GKE'
click_link 'Create on Google Kubernetes Engine'
end
context 'when user filled form with valid parameters' do
......@@ -139,7 +139,7 @@ feature 'Gcp Cluster', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
click_link 'Create on GKE'
click_link 'Create on Google Kubernetes Engine'
fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
fill_in 'cluster_name', with: 'dev-cluster'
......@@ -159,7 +159,7 @@ feature 'Gcp Cluster', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
click_link 'Create on GKE'
click_link 'Create on Google Kubernetes Engine'
fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
fill_in 'cluster_name', with: 'dev-cluster'
......@@ -177,7 +177,7 @@ feature 'Gcp Cluster', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
click_link 'Create on GKE'
click_link 'Create on Google Kubernetes Engine'
end
it 'user sees a login page' do
......
......@@ -83,7 +83,7 @@ feature 'Clusters', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
click_link 'Create on GKE'
click_link 'Create on Google Kubernetes Engine'
end
it 'user sees a login page' do
......
......@@ -7,11 +7,11 @@ describe 'Projects > Files > User uploads files' do
"You're not allowed to make changes to this project directly. "\
"A fork of this project has been created that you can make changes in, so you can submit a merge request."
end
let(:project) { create(:project, :repository, name: 'Shop') }
let(:user) { create(:user) }
let(:project) { create(:project, :repository, name: 'Shop', creator: user) }
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
let(:project_tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) }
let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) }
let(:user) { project.creator }
before do
project.add_master(user)
......
require 'spec_helper'
describe 'Project Jobs Permissions' do
let(:user) { create(:user) }
let(:group) { create(:group, name: 'some group') }
let(:project) { create(:project, :repository, namespace: group) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
let!(:job) { create(:ci_build, :running, :coverage, :trace_artifact, pipeline: pipeline) }
before do
sign_in(user)
project.enable_ci
end
describe 'jobs pages' do
shared_examples 'recent job page details responds with status' do |status|
before do
visit project_job_path(project, job)
end
it { expect(status_code).to eq(status) }
end
shared_examples 'project jobs page responds with status' do |status|
before do
visit project_jobs_path(project)
end
it { expect(status_code).to eq(status) }
end
context 'when public access for jobs is disabled' do
before do
project.update(public_builds: false)
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like 'recent job page details responds with status', 404
it_behaves_like 'project jobs page responds with status', 404
end
context 'when project is internal' do
before do
project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
it_behaves_like 'recent job page details responds with status', 404
it_behaves_like 'project jobs page responds with status', 404
end
end
context 'when public access for jobs is enabled' do
before do
project.update(public_builds: true)
end
context 'when project is internal' do
before do
project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
it_behaves_like 'recent job page details responds with status', 200 do
it 'renders job details', :js do
expect(page).to have_content "Job ##{job.id}"
expect(page).to have_css '#build-trace'
end
end
it_behaves_like 'project jobs page responds with status', 200 do
it 'renders job' do
page.within('.build') do
expect(page).to have_content("##{job.id}")
.and have_content(job.sha[0..7])
.and have_content(job.ref)
.and have_content(job.name)
end
end
end
end
end
end
describe 'artifacts page' do
context 'when recent job has artifacts available' do
before do
artifacts = Rails.root.join('spec/fixtures/ci_build_artifacts.zip')
archive = fixture_file_upload(artifacts, 'application/zip')
job.update_attributes(legacy_artifacts_file: archive)
end
context 'when public access for jobs is disabled' do
before do
project.update(public_builds: false)
end
context 'when user with guest role' do
before do
project.add_guest(user)
end
it 'responds with 404 status' do
visit download_project_job_artifacts_path(project, job)
expect(status_code).to eq(404)
end
end
context 'when user with reporter role' do
before do
project.add_reporter(user)
end
it 'starts download artifact' do
visit download_project_job_artifacts_path(project, job)
expect(status_code).to eq(200)
expect(page.response_headers['Content-Type']).to eq 'application/zip'
expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary'
end
end
end
end
end
end
......@@ -517,7 +517,7 @@ describe 'Pipelines', :js do
end
it 'creates a new pipeline' do
expect { click_on 'Create pipeline' }
expect { click_on 'Run pipeline' }
.to change { Ci::Pipeline.count }.by(1)
expect(Ci::Pipeline.last).to be_web
......@@ -526,7 +526,7 @@ describe 'Pipelines', :js do
context 'without gitlab-ci.yml' do
before do
click_on 'Create pipeline'
click_on 'Run pipeline'
end
it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
......@@ -539,7 +539,7 @@ describe 'Pipelines', :js do
click_link 'master'
end
expect { click_on 'Create pipeline' }
expect { click_on 'Run pipeline' }
.to change { Ci::Pipeline.count }.by(1)
end
end
......@@ -557,7 +557,7 @@ describe 'Pipelines', :js do
it 'has field to add a new pipeline' do
expect(page).to have_selector('.js-branch-select')
expect(find('.js-branch-select')).to have_content project.default_branch
expect(page).to have_content('Create for')
expect(page).to have_content('Run on')
end
end
......
This source diff could not be displayed because it is too large. You can view the blob instead.
import $ from 'jquery';
import DeleteModal from '~/branches/branches_delete_modal';
describe('branches delete modal', () => {
describe('setDisableDeleteButton', () => {
let submitSpy;
let $deleteButton;
beforeEach(() => {
setFixtures(`
<div id="modal-delete-branch">
<form>
<button type="submit" class="js-delete-branch">Delete</button>
</form>
</div>
`);
$deleteButton = $('.js-delete-branch');
submitSpy = jasmine.createSpy('submit').and.callFake(event => event.preventDefault());
$('#modal-delete-branch form').on('submit', submitSpy);
// eslint-disable-next-line no-new
new DeleteModal();
});
it('does not submit if button is disabled', () => {
$deleteButton.attr('disabled', true);
$deleteButton.click();
expect(submitSpy).not.toHaveBeenCalled();
});
it('submits if button is not disabled', () => {
$deleteButton.attr('disabled', false);
$deleteButton.click();
expect(submitSpy).toHaveBeenCalled();
});
});
});
import Vue from 'vue';
import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue';
import createComponent from 'spec/helpers/vue_mount_component_helper';
describe('IDE commit message field', () => {
const Component = Vue.extend(CommitMessageField);
let vm;
beforeEach(() => {
setFixtures('<div id="app"></div>');
vm = createComponent(
Component,
{
text: '',
},
'#app',
);
});
afterEach(() => {
vm.$destroy();
});
it('adds is-focused class on focus', done => {
vm.$el.querySelector('textarea').focus();
vm.$nextTick(() => {
expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
done();
});
});
it('removed is-focused class on blur', done => {
vm.$el.querySelector('textarea').focus();
vm
.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
vm.$el.querySelector('textarea').blur();
return vm.$nextTick();
})
.then(() => {
expect(vm.$el.querySelector('.is-focused')).toBeNull();
done();
})
.then(done)
.catch(done.fail);
});
it('emits input event on input', () => {
spyOn(vm, '$emit');
const textarea = vm.$el.querySelector('textarea');
textarea.value = 'testing';
textarea.dispatchEvent(new Event('input'));
expect(vm.$emit).toHaveBeenCalledWith('input', 'testing');
});
describe('highlights', () => {
describe('subject line', () => {
it('does not highlight less than 50 characters', done => {
vm.text = 'text less than 50 chars';
vm
.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.highlights span').textContent).toContain(
'text less than 50 chars',
);
expect(vm.$el.querySelector('mark').style.display).toBe('none');
})
.then(done)
.catch(done.fail);
});
it('highlights characters over 50 length', done => {
vm.text =
'text less than 50 chars that should not highlighted. text more than 50 should be highlighted';
vm
.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.highlights span').textContent).toContain(
'text less than 50 chars that should not highlighte',
);
expect(vm.$el.querySelector('mark').style.display).not.toBe('none');
expect(vm.$el.querySelector('mark').textContent).toBe(
'd. text more than 50 should be highlighted',
);
})
.then(done)
.catch(done.fail);
});
});
describe('body text', () => {
it('does not highlight body text less tan 72 characters', done => {
vm.text = 'subject line\nbody content';
vm
.$nextTick()
.then(() => {
expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
expect(vm.$el.querySelectorAll('mark')[1].style.display).toBe('none');
})
.then(done)
.catch(done.fail);
});
it('highlights body text more than 72 characters', done => {
vm.text =
'subject line\nbody content that will be highlighted when it is more than 72 characters in length';
vm
.$nextTick()
.then(() => {
expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
expect(vm.$el.querySelectorAll('mark')[1].style.display).not.toBe('none');
expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
})
.then(done)
.catch(done.fail);
});
it('highlights body text & subject line', done => {
vm.text =
'text less than 50 chars that should not highlighted\nbody content that will be highlighted when it is more than 72 characters in length';
vm
.$nextTick()
.then(() => {
expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
expect(vm.$el.querySelectorAll('mark').length).toBe(2);
expect(vm.$el.querySelectorAll('mark')[0].textContent).toContain('d');
expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
})
.then(done)
.catch(done.fail);
});
});
});
describe('scrolling textarea', () => {
it('updates transform of highlights', done => {
vm.text = 'subject line\n\n\n\n\n\n\n\n\n\n\nbody content';
vm
.$nextTick()
.then(() => {
vm.$el.querySelector('textarea').scrollTo(0, 50);
vm.handleScroll();
})
.then(vm.$nextTick)
.then(() => {
expect(vm.scrollTop).toBe(50);
expect(vm.$el.querySelector('.highlights').style.transform).toBe(
'translate3d(0px, -50px, 0px)',
);
})
.then(done)
.catch(done.fail);
});
});
});
......@@ -69,19 +69,6 @@ describe('IDE commit sidebar radio group', () => {
});
});
it('renders helpText tooltip', done => {
vm.helpText = 'help text';
Vue.nextTick(() => {
const help = vm.$el.querySelector('.help-block');
expect(help).not.toBeNull();
expect(help.getAttribute('data-original-title')).toBe('help text');
done();
});
});
describe('with input', () => {
beforeEach(done => {
vm.$destroy();
......
......@@ -55,6 +55,16 @@ describe('Multi-file store tree mutations', () => {
expect(tree.tree[1].name).toBe('submodule');
expect(tree.tree[2].name).toBe('blob');
});
it('keeps loading state', () => {
mutations.CREATE_TREE(localState, { treePath: 'project/master' });
mutations.SET_DIRECTORY_DATA(localState, {
data,
treePath: 'project/master',
});
expect(localState.trees['project/master'].loading).toBe(true);
});
});
describe('REMOVE_ALL_CHANGES_FILES', () => {
......
import Vue from 'vue';
import pipelineFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_failed';
import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue';
import { removeBreakLine } from 'spec/helpers/vue_component_helper';
describe('MRWidgetPipelineFailed', () => {
describe('PipelineFailed', () => {
describe('template', () => {
const Component = Vue.extend(pipelineFailedComponent);
const Component = Vue.extend(PipelineFailed);
const vm = new Component({
el: document.createElement('div'),
});
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(vm.$el.innerText).toContain('The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure');
expect(
removeBreakLine(vm.$el.innerText).trim(),
).toContain('The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure');
});
});
});
import Vue from 'vue';
import commitComp from '~/vue_shared/components/commit.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Commit component', () => {
let props;
......@@ -10,25 +11,28 @@ describe('Commit component', () => {
CommitComponent = Vue.extend(commitComp);
});
afterEach(() => {
component.$destroy();
});
it('should render a fork icon if it does not represent a tag', () => {
component = new CommitComponent({
propsData: {
tag: false,
commitRef: {
name: 'master',
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
},
commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
shortSha: 'b7836edd',
title: 'Commit message',
author: {
avatar_url: 'https://gitlab.com/uploads/-/system/user/avatar/300478/avatar.png',
web_url: 'https://gitlab.com/jschatz1',
path: '/jschatz1',
username: 'jschatz1',
},
component = mountComponent(CommitComponent, {
tag: false,
commitRef: {
name: 'master',
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
},
}).$mount();
commitUrl:
'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
shortSha: 'b7836edd',
title: 'Commit message',
author: {
avatar_url: 'https://gitlab.com/uploads/-/system/user/avatar/300478/avatar.png',
web_url: 'https://gitlab.com/jschatz1',
path: '/jschatz1',
username: 'jschatz1',
},
});
expect(component.$el.querySelector('.icon-container').children).toContain('svg');
});
......@@ -41,7 +45,8 @@ describe('Commit component', () => {
name: 'master',
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
},
commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
commitUrl:
'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
shortSha: 'b7836edd',
title: 'Commit message',
author: {
......@@ -53,9 +58,7 @@ describe('Commit component', () => {
commitIconSvg: '<svg></svg>',
};
component = new CommitComponent({
propsData: props,
}).$mount();
component = mountComponent(CommitComponent, props);
});
it('should render a tag icon if it represents a tag', () => {
......@@ -63,7 +66,9 @@ describe('Commit component', () => {
});
it('should render a link to the ref url', () => {
expect(component.$el.querySelector('.ref-name').getAttribute('href')).toEqual(props.commitRef.ref_url);
expect(component.$el.querySelector('.ref-name').getAttribute('href')).toEqual(
props.commitRef.ref_url,
);
});
it('should render the ref name', () => {
......@@ -71,7 +76,9 @@ describe('Commit component', () => {
});
it('should render the commit short sha with a link to the commit url', () => {
expect(component.$el.querySelector('.commit-sha').getAttribute('href')).toEqual(props.commitUrl);
expect(component.$el.querySelector('.commit-sha').getAttribute('href')).toEqual(
props.commitUrl,
);
expect(component.$el.querySelector('.commit-sha').textContent).toContain(props.shortSha);
});
......@@ -88,21 +95,25 @@ describe('Commit component', () => {
it('Should render the author avatar with title and alt attributes', () => {
expect(
component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('data-original-title'),
component.$el
.querySelector('.commit-title .avatar-image-container img')
.getAttribute('data-original-title'),
).toContain(props.author.username);
expect(
component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'),
component.$el
.querySelector('.commit-title .avatar-image-container img')
.getAttribute('alt'),
).toContain(`${props.author.username}'s avatar`);
});
});
it('should render the commit title', () => {
expect(
component.$el.querySelector('a.commit-row-message').getAttribute('href'),
).toEqual(props.commitUrl);
expect(
component.$el.querySelector('a.commit-row-message').textContent,
).toContain(props.title);
expect(component.$el.querySelector('a.commit-row-message').getAttribute('href')).toEqual(
props.commitUrl,
);
expect(component.$el.querySelector('a.commit-row-message').textContent).toContain(
props.title,
);
});
});
......@@ -114,19 +125,18 @@ describe('Commit component', () => {
name: 'master',
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
},
commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
commitUrl:
'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
shortSha: 'b7836edd',
title: null,
author: {},
};
component = new CommitComponent({
propsData: props,
}).$mount();
component = mountComponent(CommitComponent, props);
expect(
component.$el.querySelector('.commit-title span').textContent,
).toContain('Cant find HEAD commit for this branch');
expect(component.$el.querySelector('.commit-title span').textContent).toContain(
"Can't find HEAD commit for this branch",
);
});
});
});
......@@ -11,7 +11,7 @@ describe Banzai::ObjectRenderer do
)
end
let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_VERSION) }
let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
describe '#render' do
context 'with cache' do
......
......@@ -458,7 +458,7 @@ describe Gitlab::Ci::Trace do
context 'when job does not have trace artifact' do
context 'when trace file stored in default path' do
let!(:build) { create(:ci_build, :success, :trace_live) }
let!(:src_path) { trace.read { |s| return s.path } }
let!(:src_path) { trace.read { |s| s.path } }
let!(:src_checksum) { Digest::SHA256.file(src_path).hexdigest }
it_behaves_like 'archive trace file'
......
......@@ -79,6 +79,8 @@ describe Gitlab::Diff::Highlight do
end
it 'keeps the original rich line' do
allow(Gitlab::Sentry).to receive(:track_exception)
code = %q{+ raise RuntimeError, "System commands must be given as an array of strings"}
expect(subject[5].text).to eq(code)
......@@ -86,12 +88,9 @@ describe Gitlab::Diff::Highlight do
end
it 'reports to Sentry if configured' do
allow(Gitlab::Sentry).to receive(:enabled?).and_return(true)
expect(Gitlab::Sentry).to receive(:context)
expect(Raven).to receive(:capture_exception)
expect(Gitlab::Sentry).to receive(:track_exception).and_call_original
subject
expect { subject }. to raise_exception(RangeError)
end
end
end
......
......@@ -470,9 +470,20 @@ describe Gitlab::Git::Repository, seed_helper: true do
FileUtils.rm_rf(heads_dir)
FileUtils.mkdir_p(heads_dir)
repository.expire_has_local_branches_cache
expect(repository.has_local_branches?).to eq(false)
end
end
context 'memoizes the value' do
it 'returns true' do
expect(repository).to receive(:uncached_has_local_branches?).once.and_call_original
2.times do
expect(repository.has_local_branches?).to eq(true)
end
end
end
end
context 'with gitaly' do
......
......@@ -7,7 +7,49 @@ describe Gitlab::Sentry do
described_class.context(nil)
expect(Raven.tags_context[:locale]).to eq(I18n.locale.to_s)
expect(Raven.tags_context[:locale].to_s).to eq(I18n.locale.to_s)
end
end
describe '.track_exception' do
let(:exception) { RuntimeError.new('boom') }
before do
allow(described_class).to receive(:enabled?).and_return(true)
end
it 'raises the exception if it should' do
expect(described_class).to receive(:should_raise?).and_return(true)
expect { described_class.track_exception(exception) }
.to raise_error(RuntimeError)
end
context 'when exceptions should not be raised' do
before do
allow(described_class).to receive(:should_raise?).and_return(false)
end
it 'logs the exception with all attributes passed' do
expected_extras = {
some_other_info: 'info',
issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1'
}
expect(Raven).to receive(:capture_exception)
.with(exception, extra: a_hash_including(expected_extras))
described_class.track_exception(
exception,
issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1',
extra: { some_other_info: 'info' }
)
end
it 'sets the context' do
expect(described_class).to receive(:context)
described_class.track_exception(exception)
end
end
end
end
......@@ -727,7 +727,7 @@ describe Gitlab::Shell do
def find_in_authorized_keys_file(key_id)
gitlab_shell.batch_read_key_ids do |ids|
return true if ids.include?(key_id)
return true if ids.include?(key_id) # rubocop:disable Cop/AvoidReturnFromBlocks
end
false
......
......@@ -2140,10 +2140,6 @@ describe Ci::Build do
it "doesn't save timeout_source" do
expect { run_job_without_exception }.not_to change { job.reload.ensure_metadata.timeout_source }
end
it 'raises an exception' do
expect { job.run! }.to raise_error(StateMachines::InvalidTransition)
end
end
end
......
......@@ -450,6 +450,11 @@ eos
it "returns nil if the path doesn't exists" do
expect(commit.uri_type('this/path/doesnt/exist')).to be_nil
end
it 'is nil if the path is nil or empty' do
expect(commit.uri_type(nil)).to be_nil
expect(commit.uri_type("")).to be_nil
end
end
context 'when Gitaly commit_tree_entry feature is enabled' do
......
......@@ -533,4 +533,36 @@ describe CommitStatus do
end
end
end
describe '#enqueue' do
let!(:current_time) { Time.new(2018, 4, 5, 14, 0, 0) }
before do
allow(Time).to receive(:now).and_return(current_time)
end
shared_examples 'commit status enqueued' do
it 'sets queued_at value when enqueued' do
expect { commit_status.enqueue }.to change { commit_status.reload.queued_at }.from(nil).to(current_time)
end
end
context 'when initial state is :created' do
let(:commit_status) { create(:commit_status, :created) }
it_behaves_like 'commit status enqueued'
end
context 'when initial state is :skipped' do
let(:commit_status) { create(:commit_status, :skipped) }
it_behaves_like 'commit status enqueued'
end
context 'when initial state is :manual' do
let(:commit_status) { create(:commit_status, :manual) }
it_behaves_like 'commit status enqueued'
end
end
end
......@@ -72,7 +72,7 @@ describe CacheMarkdownField do
let(:updated_markdown) { '`Bar`' }
let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' }
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_VERSION) }
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
describe '.attributes' do
it 'excludes cache attributes' do
......@@ -89,17 +89,24 @@ describe CacheMarkdownField do
it { expect(thing.foo).to eq(markdown) }
it { expect(thing.foo_html).to eq(html) }
it { expect(thing.foo_html_changed?).not_to be_truthy }
it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
end
context 'a changed markdown field' do
before do
thing.foo = updated_markdown
thing.save
shared_examples 'with cache version' do |cache_version|
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
before do
thing.foo = updated_markdown
thing.save
end
it { expect(thing.foo_html).to eq(updated_html) }
it { expect(thing.cached_markdown_version).to eq(cache_version) }
end
it { expect(thing.foo_html).to eq(updated_html) }
it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
end
context 'when a markdown field is set repeatedly to an empty string' do
......@@ -123,15 +130,22 @@ describe CacheMarkdownField do
end
context 'a non-markdown field changed' do
before do
thing.bar = 'OK'
thing.save
shared_examples 'with cache version' do |cache_version|
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
before do
thing.bar = 'OK'
thing.save
end
it { expect(thing.bar).to eq('OK') }
it { expect(thing.foo).to eq(markdown) }
it { expect(thing.foo_html).to eq(html) }
it { expect(thing.cached_markdown_version).to eq(cache_version) }
end
it { expect(thing.bar).to eq('OK') }
it { expect(thing.foo).to eq(markdown) }
it { expect(thing.foo_html).to eq(html) }
it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
end
context 'version is out of date' do
......@@ -142,59 +156,85 @@ describe CacheMarkdownField do
end
it { expect(thing.foo_html).to eq(updated_html) }
it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION) }
end
describe '#cached_html_up_to_date?' do
subject { thing.cached_html_up_to_date?(:foo) }
shared_examples 'with cache version' do |cache_version|
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
it 'returns false when the version is absent' do
thing.cached_markdown_version = nil
subject { thing.cached_html_up_to_date?(:foo) }
is_expected.to be_falsy
end
it 'returns false when the version is absent' do
thing.cached_markdown_version = nil
it 'returns false when the version is too early' do
thing.cached_markdown_version -= 1
is_expected.to be_falsy
end
is_expected.to be_falsy
end
it 'returns false when the version is too early' do
thing.cached_markdown_version -= 1
it 'returns false when the version is too late' do
thing.cached_markdown_version += 1
is_expected.to be_falsy
end
is_expected.to be_falsy
end
it 'returns false when the version is too late' do
thing.cached_markdown_version += 1
it 'returns true when the version is just right' do
thing.cached_markdown_version = CacheMarkdownField::CACHE_VERSION
is_expected.to be_falsy
end
is_expected.to be_truthy
end
it 'returns true when the version is just right' do
thing.cached_markdown_version = cache_version
it 'returns false if markdown has been changed but html has not' do
thing.foo = updated_html
is_expected.to be_truthy
end
is_expected.to be_falsy
end
it 'returns false if markdown has been changed but html has not' do
thing.foo = updated_html
it 'returns true if markdown has not been changed but html has' do
thing.foo_html = updated_html
is_expected.to be_falsy
end
it 'returns true if markdown has not been changed but html has' do
thing.foo_html = updated_html
is_expected.to be_truthy
is_expected.to be_truthy
end
it 'returns true if markdown and html have both been changed' do
thing.foo = updated_markdown
thing.foo_html = updated_html
is_expected.to be_truthy
end
it 'returns false if the markdown field is set but the html is not' do
thing.foo_html = nil
is_expected.to be_falsy
end
end
it 'returns true if markdown and html have both been changed' do
thing.foo = updated_markdown
thing.foo_html = updated_html
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
end
describe '#latest_cached_markdown_version' do
subject { thing.latest_cached_markdown_version }
is_expected.to be_truthy
it 'returns redcarpet version' do
thing.cached_markdown_version = CacheMarkdownField::CACHE_COMMONMARK_VERSION_START - 1
is_expected.to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
it 'returns false if the markdown field is set but the html is not' do
thing.foo_html = nil
it 'returns commonmark version' do
thing.cached_markdown_version = CacheMarkdownField::CACHE_COMMONMARK_VERSION_START + 1
is_expected.to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION)
end
is_expected.to be_falsy
it 'returns default version when version is nil' do
thing.cached_markdown_version = nil
is_expected.to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
end
......@@ -221,37 +261,44 @@ describe CacheMarkdownField do
thing.cached_markdown_version = nil
thing.refresh_markdown_cache
expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
end
describe '#refresh_markdown_cache!' do
before do
thing.foo = updated_markdown
end
shared_examples 'with cache version' do |cache_version|
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
it 'fills all html fields' do
thing.refresh_markdown_cache!
before do
thing.foo = updated_markdown
end
expect(thing.foo_html).to eq(updated_html)
expect(thing.foo_html_changed?).to be_truthy
expect(thing.baz_html_changed?).to be_truthy
end
it 'fills all html fields' do
thing.refresh_markdown_cache!
it 'skips saving if not persisted' do
expect(thing).to receive(:persisted?).and_return(false)
expect(thing).not_to receive(:update_columns)
expect(thing.foo_html).to eq(updated_html)
expect(thing.foo_html_changed?).to be_truthy
expect(thing.baz_html_changed?).to be_truthy
end
thing.refresh_markdown_cache!
end
it 'skips saving if not persisted' do
expect(thing).to receive(:persisted?).and_return(false)
expect(thing).not_to receive(:update_columns)
it 'saves the changes using #update_columns' do
expect(thing).to receive(:persisted?).and_return(true)
expect(thing).to receive(:update_columns)
.with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION)
thing.refresh_markdown_cache!
end
thing.refresh_markdown_cache!
it 'saves the changes using #update_columns' do
expect(thing).to receive(:persisted?).and_return(true)
expect(thing).to receive(:update_columns)
.with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => cache_version)
thing.refresh_markdown_cache!
end
end
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
end
describe '#banzai_render_context' do
......@@ -299,7 +346,7 @@ describe CacheMarkdownField do
expect(thing.foo_html).to eq(updated_html)
expect(thing.baz_html).to eq(updated_html)
expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
end
......@@ -319,7 +366,7 @@ describe CacheMarkdownField do
expect(thing.foo_html).to eq(updated_html)
expect(thing.baz_html).to eq(updated_html)
expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
end
end
......
......@@ -1437,6 +1437,12 @@ describe Repository do
repository.expire_emptiness_caches
end
it 'expires the memoized repository cache' do
allow(repository.raw_repository).to receive(:expire_has_local_branches_cache).and_call_original
repository.expire_emptiness_caches
end
end
describe 'skip_merges option' do
......
......@@ -427,5 +427,20 @@ describe API::Repositories do
let(:request) { get api(route, guest) }
end
end
# Regression: https://gitlab.com/gitlab-org/gitlab-ce/issues/45363
describe 'Links header contains working URLs when no `order_by` nor `sort` is given' do
let(:project) { create(:project, :public, :repository) }
let(:current_user) { nil }
it 'returns `Link` header that includes URLs with default value for `order_by` & `sort`' do
get api(route, current_user)
first_link_url = response.headers['Link'].split(';').first
expect(first_link_url).to include('order_by=commits')
expect(first_link_url).to include('sort=asc')
end
end
end
end
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/avoid_break_from_strong_memoize'
describe RuboCop::Cop::AvoidBreakFromStrongMemoize do
include CopHelper
subject(:cop) { described_class.new }
it 'flags violation for break inside strong_memoize' do
expect_offense(<<~RUBY)
strong_memoize(:result) do
break if something
^^^^^ Do not use break inside strong_memoize, use next instead.
do_an_heavy_calculation
end
RUBY
end
it 'flags violation for break inside strong_memoize nested blocks' do
expect_offense(<<~RUBY)
strong_memoize do
items.each do |item|
break item
^^^^^^^^^^ Do not use break inside strong_memoize, use next instead.
end
end
RUBY
end
it "doesn't flag violation for next inside strong_memoize" do
expect_no_offenses(<<~RUBY)
strong_memoize(:result) do
next if something
do_an_heavy_calculation
end
RUBY
end
it "doesn't flag violation for break inside blocks" do
expect_no_offenses(<<~RUBY)
call do
break if something
do_an_heavy_calculation
end
RUBY
end
it "doesn't call add_offense twice for nested blocks" do
source = <<~RUBY
call do
strong_memoize(:result) do
break if something
do_an_heavy_calculation
end
end
RUBY
expect_any_instance_of(described_class).to receive(:add_offense).once
inspect_source(source)
end
it "doesn't check when block is empty" do
expect_no_offenses(<<~RUBY)
strong_memoize(:result) do
end
RUBY
end
end
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/avoid_return_from_blocks'
describe RuboCop::Cop::AvoidReturnFromBlocks do
include CopHelper
subject(:cop) { described_class.new }
it 'flags violation for return inside a block' do
expect_offense(<<~RUBY)
call do
do_something
return if something_else
^^^^^^ Do not return from a block, use next or break instead.
end
RUBY
end
it "doesn't call add_offense twice for nested blocks" do
source = <<~RUBY
call do
call do
something
return if something_else
end
end
RUBY
expect_any_instance_of(described_class).to receive(:add_offense).once
inspect_source(source)
end
it 'flags violation for return inside included > def > block' do
expect_offense(<<~RUBY)
included do
def a_method
return if something
call do
return if something_else
^^^^^^ Do not return from a block, use next or break instead.
end
end
end
RUBY
end
shared_examples 'examples with whitelisted method' do |whitelisted_method|
it "doesn't flag violation for return inside #{whitelisted_method}" do
expect_no_offenses(<<~RUBY)
items.#{whitelisted_method} do |item|
do_something
return if something_else
end
RUBY
end
end
%i[each each_filename times loop].each do |whitelisted_method|
it_behaves_like 'examples with whitelisted method', whitelisted_method
end
shared_examples 'examples with def methods' do |def_method|
it "doesn't flag violation for return inside #{def_method}" do
expect_no_offenses(<<~RUBY)
helpers do
#{def_method} do
return if something
do_something_more
end
end
RUBY
end
end
%i[define_method lambda].each do |def_method|
it_behaves_like 'examples with def methods', def_method
end
it "doesn't flag violation for return inside a lambda" do
expect_no_offenses(<<~RUBY)
lambda do
do_something
return if something_else
end
RUBY
end
it "doesn't flag violation for return used inside a method definition" do
expect_no_offenses(<<~RUBY)
describe Klass do
def a_method
do_something
return if something_else
end
end
RUBY
end
it "doesn't flag violation for next inside a block" do
expect_no_offenses(<<~RUBY)
call do
do_something
next if something_else
end
RUBY
end
it "doesn't flag violation for break inside a block" do
expect_no_offenses(<<~RUBY)
call do
do_something
break if something_else
end
RUBY
end
it "doesn't check when block is empty" do
expect_no_offenses(<<~RUBY)
call do
end
RUBY
end
end
......@@ -370,10 +370,111 @@ module Ci
it_behaves_like 'validation is not active'
end
end
end
describe '#register_success' do
let!(:current_time) { Time.new(2018, 4, 5, 14, 0, 0) }
let!(:attempt_counter) { double('Gitlab::Metrics::NullMetric') }
let!(:job_queue_duration_seconds) { double('Gitlab::Metrics::NullMetric') }
before do
allow(Time).to receive(:now).and_return(current_time)
# Stub defaults for any metrics other than the ones we're testing
allow(Gitlab::Metrics).to receive(:counter)
.with(any_args)
.and_return(Gitlab::Metrics::NullMetric.instance)
allow(Gitlab::Metrics).to receive(:histogram)
.with(any_args)
.and_return(Gitlab::Metrics::NullMetric.instance)
# Stub tested metrics
allow(Gitlab::Metrics).to receive(:counter)
.with(:job_register_attempts_total, anything)
.and_return(attempt_counter)
allow(Gitlab::Metrics).to receive(:histogram)
.with(:job_queue_duration_seconds, anything, anything, anything)
.and_return(job_queue_duration_seconds)
project.update(shared_runners_enabled: true)
pending_job.update(created_at: current_time - 3600, queued_at: current_time - 1800)
end
shared_examples 'attempt counter collector' do
it 'increments attempt counter' do
allow(job_queue_duration_seconds).to receive(:observe)
expect(attempt_counter).to receive(:increment)
execute(runner)
end
end
shared_examples 'jobs queueing time histogram collector' do
it 'counts job queuing time histogram with expected labels' do
allow(attempt_counter).to receive(:increment)
expect(job_queue_duration_seconds).to receive(:observe)
.with({ shared_runner: expected_shared_runner,
jobs_running_for_project: expected_jobs_running_for_project_first_job }, 1800)
execute(runner)
end
context 'when project already has running jobs' do
let!(:build2) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) }
let!(:build3) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) }
it 'counts job queuing time histogram with expected labels' do
allow(attempt_counter).to receive(:increment)
expect(job_queue_duration_seconds).to receive(:observe)
.with({ shared_runner: expected_shared_runner,
jobs_running_for_project: expected_jobs_running_for_project_third_job }, 1800)
execute(runner)
end
end
end
def execute(runner)
described_class.new(runner).execute.build
shared_examples 'metrics collector' do
it_behaves_like 'attempt counter collector'
it_behaves_like 'jobs queueing time histogram collector'
end
context 'when shared runner is used' do
let(:runner) { shared_runner }
let(:expected_shared_runner) { true }
let(:expected_jobs_running_for_project_first_job) { 0 }
let(:expected_jobs_running_for_project_third_job) { 2 }
it_behaves_like 'metrics collector'
context 'when pending job with queued_at=nil is used' do
before do
pending_job.update(queued_at: nil)
end
it_behaves_like 'attempt counter collector'
it "doesn't count job queuing time histogram" do
allow(attempt_counter).to receive(:increment)
expect(job_queue_duration_seconds).not_to receive(:observe)
execute(runner)
end
end
end
context 'when specific runner is used' do
let(:runner) { specific_runner }
let(:expected_shared_runner) { false }
let(:expected_jobs_running_for_project_first_job) { '+Inf' }
let(:expected_jobs_running_for_project_third_job) { '+Inf' }
it_behaves_like 'metrics collector'
end
end
def execute(runner)
described_class.new(runner).execute.build
end
end
end
......@@ -933,6 +933,46 @@ describe NotificationService, :mailer do
let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) }
end
end
describe '#issue_due' do
before do
issue.update!(due_date: Date.today)
update_custom_notification(:issue_due, @u_guest_custom, resource: project)
update_custom_notification(:issue_due, @u_custom_global)
end
it 'sends email to issue notification recipients, excluding watchers' do
notification.issue_due(issue)
should_email(issue.assignees.first)
should_email(issue.author)
should_email(@u_guest_custom)
should_email(@u_custom_global)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
should_not_email(@u_watcher)
should_not_email(@u_guest_watcher)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
should_not_email(@u_lazy_participant)
end
it 'sends the email from the author' do
notification.issue_due(issue)
email = find_email_for(@subscriber)
expect(email.header[:from].display_names).to eq([issue.author.name])
end
it_behaves_like 'participating notifications' do
let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { issue }
let(:notification_trigger) { notification.issue_due(issue) }
end
end
end
describe 'Merge Requests' do
......
......@@ -909,7 +909,13 @@ describe SystemNoteService do
it 'sets the note text' do
noteable.update_attribute(:time_estimate, 277200)
expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
expect(subject.note).to eq "changed time estimate to 1w 4d 5h,"
end
it 'appends a comma to separate the note from the update_at time' do
noteable.update_attribute(:time_estimate, 277200)
expect(subject.note).to end_with(',')
end
end
......
......@@ -44,10 +44,11 @@ module HttpIOHelpers
def remote_trace_body
@remote_trace_body ||= File.read(expand_fixture_path('trace/sample_trace'))
.force_encoding(Encoding::BINARY)
end
def remote_trace_size
remote_trace_body.length
remote_trace_body.bytesize
end
def set_smaller_buffer_size_than(file_size)
......
......@@ -4,7 +4,7 @@ shared_examples "matches the method pattern" do |method|
let(:pattern) { patterns[method] }
it do
return skip "No pattern provided, skipping." unless pattern
skip "No pattern provided, skipping." unless pattern
expect(target.method(method).call(*args)).to match(pattern)
end
......
require 'spec_helper'
describe IssueDueSchedulerWorker do
describe '#perform' do
it 'schedules one MailScheduler::IssueDueWorker per project with open issues due tomorrow' do
project1 = create(:project)
project2 = create(:project)
project_closed_issue = create(:project)
project_issue_due_another_day = create(:project)
create(:issue, :opened, project: project1, due_date: Date.tomorrow)
create(:issue, :opened, project: project1, due_date: Date.tomorrow)
create(:issue, :opened, project: project2, due_date: Date.tomorrow)
create(:issue, :closed, project: project_closed_issue, due_date: Date.tomorrow)
create(:issue, :opened, project: project_issue_due_another_day, due_date: Date.today)
expect(MailScheduler::IssueDueWorker).to receive(:bulk_perform_async).with([[project1.id], [project2.id]])
described_class.new.perform
end
end
end
require 'spec_helper'
describe MailScheduler::IssueDueWorker do
describe '#perform' do
let(:worker) { described_class.new }
let(:project) { create(:project) }
it 'sends emails for open issues due tomorrow in the project specified' do
issue1 = create(:issue, :opened, project: project, due_date: Date.tomorrow)
issue2 = create(:issue, :opened, project: project, due_date: Date.tomorrow)
create(:issue, :closed, project: project, due_date: Date.tomorrow) # closed
create(:issue, :opened, project: project, due_date: 2.days.from_now) # due on another day
create(:issue, :opened, due_date: Date.tomorrow) # different project
expect_any_instance_of(NotificationService).to receive(:issue_due).with(issue1)
expect_any_instance_of(NotificationService).to receive(:issue_due).with(issue2)
worker.perform(project.id)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment