Merge branch 'master' into 'bootstrap4'
# Conflicts: # app/views/projects/issues/_nav_btns.html.haml # app/views/projects/merge_requests/creations/_new_compare.html.haml
This commit is contained in:
commit
cad4eb989e
146 changed files with 4953 additions and 1517 deletions
|
@ -36,6 +36,7 @@ Set the title to: `[Security] Description of the original issue`
|
|||
- [ ] 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
|
||||
|
@ -61,8 +62,9 @@ Set the title to: `[Security] Description of the original issue`
|
|||
| 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/process.md
|
||||
[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
|
||||
|
|
|
@ -1,41 +1,27 @@
|
|||
<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>
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
<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>
|
|
@ -1,52 +1,40 @@
|
|||
<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)"
|
||||
/>
|
||||
|
|
|
@ -5,6 +5,7 @@ import icon from '~/vue_shared/components/icon.vue';
|
|||
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 CommitMessageField from './commit_sidebar/message_field.vue';
|
||||
import * as consts from '../stores/modules/commit/constants';
|
||||
import Actions from './commit_sidebar/actions.vue';
|
||||
|
||||
|
@ -15,6 +16,7 @@ export default {
|
|||
commitFilesList,
|
||||
Actions,
|
||||
LoadingButton,
|
||||
CommitMessageField,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
|
@ -38,15 +40,9 @@ export default {
|
|||
'changedFiles',
|
||||
]),
|
||||
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
|
||||
...mapGetters('commit', [
|
||||
'commitButtonDisabled',
|
||||
'discardDraftButtonDisabled',
|
||||
'branchName',
|
||||
]),
|
||||
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']),
|
||||
statusSvg() {
|
||||
return this.lastCommitMsg
|
||||
? this.committedStateSvgPath
|
||||
: this.noChangesStateSvgPath;
|
||||
return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -64,9 +60,7 @@ export default {
|
|||
});
|
||||
},
|
||||
forceCreateNewBranch() {
|
||||
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() =>
|
||||
this.commitChanges(),
|
||||
);
|
||||
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -105,16 +99,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
|
||||
|
|
3
app/assets/javascripts/ide/constants.js
Normal file
3
app/assets/javascripts/ide/constants.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
// Fuzzy file finder
|
||||
export const MAX_TITLE_LENGTH = 50;
|
||||
export const MAX_BODY_LENGTH = 72;
|
|
@ -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]);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -662,11 +662,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.multi-file-commit-message.form-control {
|
||||
height: 160px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.dirty-diff {
|
||||
// !important need to override monaco inline style
|
||||
width: 4px !important;
|
||||
|
@ -839,3 +834,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
|
||||
|
|
11
app/helpers/safe_params_helper.rb
Normal file
11
app/helpers/safe_params_helper.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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?
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- 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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<%= yield -%>
|
||||
|
||||
---
|
||||
-- <%# signature marker %>
|
||||
You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<%= yield -%>
|
||||
|
||||
---
|
||||
-- <%# signature marker %>
|
||||
<% if @target_url -%>
|
||||
<% if @reply_by_email -%>
|
||||
<%= "Reply to this email directly or view it on GitLab: #{@target_url}" -%>
|
||||
|
|
12
app/views/notify/issue_due_email.html.haml
Normal file
12
app/views/notify/issue_due_email.html.haml
Normal file
|
@ -0,0 +1,12 @@
|
|||
%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)
|
7
app/views/notify/issue_due_email.text.erb
Normal file
7
app/views/notify/issue_due_email.text.erb
Normal file
|
@ -0,0 +1,7 @@
|
|||
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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- 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.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= link_to params.merge(rss_url_options), class: 'btn btn-secondary append-right-10 has-tooltip', title: 'Subscribe' do
|
||||
= link_to safe_params.merge(rss_url_options), class: 'btn btn-secondary append-right-10 has-tooltip', title: 'Subscribe' do
|
||||
= icon('rss')
|
||||
- if @can_bulk_update
|
||||
= button_tag "Edit issues", class: "btn btn-secondary append-right-10 js-bulk-update-toggle"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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|
|
||||
.d-none.alert.alert-danger.mr-compare-errors
|
||||
.merge-request-branches.js-merge-request-new-compare.row.col-md-12{ '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.col-md-12{ '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.pl-0
|
||||
.card.card-new-merge-request
|
||||
.card-header
|
||||
|
|
|
@ -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.badge-pill= @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.badge-pill= @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.badge-pill= @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
|
||||
|
|
|
@ -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
|
||||
|
|
7
app/workers/concerns/mail_scheduler_queue.rb
Normal file
7
app/workers/concerns/mail_scheduler_queue.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
module MailSchedulerQueue
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
queue_namespace :mail_scheduler
|
||||
end
|
||||
end
|
10
app/workers/issue_due_scheduler_worker.rb
Normal file
10
app/workers/issue_due_scheduler_worker.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
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
|
14
app/workers/mail_scheduler/issue_due_worker.rb
Normal file
14
app/workers/mail_scheduler/issue_due_worker.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
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
|
||||
|
|
5
changelogs/unreleased/16957-issue-due-email.yml
Normal file
5
changelogs/unreleased/16957-issue-due-email.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add cron job to email users on issue due date
|
||||
merge_request: 17985
|
||||
author: Stuart Nelson
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add a comma to the time estimate system notes
|
||||
merge_request: 18326
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove ahead/behind graphs on project branches on mobile
|
||||
merge_request: 18415
|
||||
author: Takuya Noguchi
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Rubocop rule to avoid returning from a block
|
||||
merge_request: 18000
|
||||
author: Jacopo Beschi @jacopo-beschi
|
||||
type: added
|
5
changelogs/unreleased/43617-mailsig.yml
Normal file
5
changelogs/unreleased/43617-mailsig.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Use RFC 3676 mail signature delimiters
|
||||
merge_request: 17979
|
||||
author: Enrico Scholz
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix `Trace::HttpIO` can not render multi-byte chars
|
||||
merge_request: 18417
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Partition job_queue_duration_seconds with jobs_running_for_project
|
||||
merge_request: 17730
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/zj-ref-exists-opt-out.yml
Normal file
5
changelogs/unreleased/zj-ref-exists-opt-out.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
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
|
||||
#
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
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
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# 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,
|
||||
|
|
|
@ -133,11 +133,19 @@ roughly be as follows:
|
|||
1. Release B:
|
||||
1. Deploy code so that the application starts using the new column and stops
|
||||
scheduling jobs for newly created data.
|
||||
1. In a post-deployment migration you'll need to ensure no jobs remain. To do
|
||||
so you can use `Gitlab::BackgroundMigration.steal` to process any remaining
|
||||
jobs before continuing.
|
||||
1. In a post-deployment migration you'll need to ensure no jobs remain.
|
||||
1. Use `Gitlab::BackgroundMigration.steal` to process any remaining
|
||||
jobs in Sidekiq.
|
||||
1. Reschedule the migration to be run directly (i.e. not through Sidekiq)
|
||||
on any rows that weren't migrated by Sidekiq. This can happen if, for
|
||||
instance, Sidekiq received a SIGKILL, or if a particular batch failed
|
||||
enough times to be marked as dead.
|
||||
1. Remove the old column.
|
||||
|
||||
This may also require a bump to the [import/export version][import-export], if
|
||||
importing a project from a prior version of GitLab requires the data to be in
|
||||
the new format.
|
||||
|
||||
## Example
|
||||
|
||||
To explain all this, let's use the following example: the table `services` has a
|
||||
|
@ -296,3 +304,4 @@ for more details.
|
|||
[migrations-readme]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md
|
||||
[issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab-ce/issues/35351
|
||||
[reliable-sidekiq]: https://gitlab.com/gitlab-org/gitlab-ce/issues/36791
|
||||
[import-export]: ../user/project/settings/import_export.md
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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).
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# 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**
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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'
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue