Merge branch 'master' into ide-staged-changes
This commit is contained in:
commit
fb3e23b06d
|
@ -0,0 +1,70 @@
|
|||
<!--
|
||||
# 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
|
2
Gemfile
2
Gemfile
|
@ -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('');
|
||||
|
|
|
@ -1,96 +1,102 @@
|
|||
<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"
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<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,
|
||||
|
|
|
@ -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)"
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
// 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) {
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
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>
|
||||
`,
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
<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
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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-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"
|
||||
|
|
|
@ -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|
|
||||
.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
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
- 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 }})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.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
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
= 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
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
module MailSchedulerQueue
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
queue_namespace :mail_scheduler
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
#!/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
|
|
@ -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: Improves wording in new pipeline page
|
||||
merge_request:
|
||||
author:
|
||||
type: other
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Breaks commit not found message in pipelines table
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace GKE acronym with Google Kubernetes Engine
|
||||
merge_request:
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix `Trace::HttpIO` can not render multi-byte chars
|
||||
merge_request: 18417
|
||||
author:
|
||||
type: fixed
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue