Commit fb3e23b0 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into ide-staged-changes

parents 35d28045 9c2f6e04
<!--
# Read me first!
Create this issue under https://dev.gitlab.org/gitlab/gitlabhq
Set the title to: `[Security] Description of the original issue`
-->
### Prior to the security release
- [ ] Read the [security process for developers] if you are not familiar with it.
- [ ] Link to the original issue adding it to the [links section](#links)
- [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org`
- [ ] Create an MR targetting `org` `master`, prefixing your branch with `security-`
- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]`
- [ ] Add a link to the MR to the [links section](#links)
- [ ] Add a link to an EE MR if required
- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
- [ ] Assign the MR to a RM once is reviewed and ready to be merged. Check the [RM list] to see who to ping.
#### Backports
- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases
- [ ] At this point, it might be easy to squash the commits from the MR into one
- You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [seckpick documentation]
- [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable)
- [ ] Create each MR targetting the security branch `security-X-Y`
- [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/process.md#secpick-script
#### Documentation and final details
- [ ] Check the topic on #security to see when the next release is going ot happen and add a link to the [links section](#links)
- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
### Summary
#### Links
| Description | Link |
| -------- | -------- |
| Original issue | #TODO |
| Security release issue | #TODO |
| `master` MR | !TODO |
| `master` MR (EE) | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR (EE) | !TODO |
| `Backport X.Y` MR (EE) | !TODO |
| `Backport X.Y` MR (EE) | !TODO |
#### Details
| Description | Details | Further details|
| -------- | -------- | -------- |
| Versions affected | X.Y | |
| Upgrade notes | | |
| GitLab Settings updated | Yes/No| |
| Migration required | Yes/No | |
| Thanks | | |
[security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md
[RM list]: https://about.gitlab.com/release-managers/
/label ~security
...@@ -62,7 +62,7 @@ gem 'akismet', '~> 2.0' ...@@ -62,7 +62,7 @@ gem 'akismet', '~> 2.0'
# Two-factor authentication # Two-factor authentication
gem 'devise-two-factor', '~> 3.0.0' gem 'devise-two-factor', '~> 3.0.0'
gem 'rqrcode-rails3', '~> 0.1.7' gem 'rqrcode-rails3', '~> 0.1.7'
gem 'attr_encrypted', '~> 3.0.0' gem 'attr_encrypted', '~> 3.1.0'
gem 'u2f', '~> 0.2.1' gem 'u2f', '~> 0.2.1'
# GitLab Pages # GitLab Pages
......
...@@ -66,7 +66,7 @@ GEM ...@@ -66,7 +66,7 @@ GEM
unf unf
ast (2.4.0) ast (2.4.0)
atomic (1.1.99) atomic (1.1.99)
attr_encrypted (3.0.3) attr_encrypted (3.1.0)
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
attr_required (1.0.0) attr_required (1.0.0)
autoprefixer-rails (6.2.3) autoprefixer-rails (6.2.3)
...@@ -1004,7 +1004,7 @@ DEPENDENCIES ...@@ -1004,7 +1004,7 @@ DEPENDENCIES
asciidoctor (~> 1.5.6) asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8) asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.2.0) asset_sync (~> 2.2.0)
attr_encrypted (~> 3.0.0) attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0) awesome_print (~> 1.2.0)
babosa (~> 1.0.2) babosa (~> 1.0.2)
base32 (~> 0.3.0) base32 (~> 0.3.0)
......
...@@ -16,6 +16,7 @@ class DeleteModal { ...@@ -16,6 +16,7 @@ class DeleteModal {
bindEvents() { bindEvents() {
this.$toggleBtns.on('click', this.setModalData.bind(this)); this.$toggleBtns.on('click', this.setModalData.bind(this));
this.$confirmInput.on('input', this.setDeleteDisabled.bind(this)); this.$confirmInput.on('input', this.setDeleteDisabled.bind(this));
this.$deleteBtn.on('click', this.setDisableDeleteButton.bind(this));
} }
setModalData(e) { setModalData(e) {
...@@ -30,6 +31,16 @@ class DeleteModal { ...@@ -30,6 +31,16 @@ class DeleteModal {
this.$deleteBtn.attr('disabled', e.currentTarget.value !== this.branchName); 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() { updateModal() {
this.$branchName.text(this.branchName); this.$branchName.text(this.branchName);
this.$confirmInput.val(''); this.$confirmInput.val('');
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue'; import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { import { APPLICATION_INSTALLED, INGRESS } from '../constants';
APPLICATION_INSTALLED,
INGRESS,
} from '../constants';
export default { export default {
components: { components: {
applicationRow, applicationRow,
clipboardButton, clipboardButton,
},
props: {
applications: {
type: Object,
required: false,
default: () => ({}),
}, },
props: { helpPath: {
applications: { type: String,
type: Object, required: false,
required: false, default: '',
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: '',
},
}, },
computed: { ingressHelpPath: {
generalApplicationDescription() { type: String,
return sprintf( required: false,
_.escape(s__( 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. `ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`, Read more about %{helpLink}`,
)), { ),
helpLink: `<a href="${this.helpPath}"> ),
{
helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))} ${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`, </a>`,
}, },
false, false,
); );
}, },
ingressId() { ingressId() {
return INGRESS; return INGRESS;
}, },
ingressInstalled() { ingressInstalled() {
return this.applications.ingress.status === APPLICATION_INSTALLED; return this.applications.ingress.status === APPLICATION_INSTALLED;
}, },
ingressExternalIp() { ingressExternalIp() {
return this.applications.ingress.externalIp; return this.applications.ingress.externalIp;
}, },
ingressDescription() { ingressDescription() {
const extraCostParagraph = sprintf( const extraCostParagraph = sprintf(
_.escape(s__( _.escape(
s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources `ClusterIntegration|%{boldNotice} This will add some extra resources
like a load balancer, which may incur additional costs depending on 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, the hosting provider your Kubernetes cluster is installed on. If you are using
you can %{pricingLink}.`, 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"> {
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>`, ${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`,
}, },
false, false,
); );
const externalIpParagraph = sprintf( const externalIpParagraph = sprintf(
_.escape(s__( _.escape(
s__(
`ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS `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}`, 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'))} ${_.escape(s__('ClusterIntegration|More information'))}
</a>`, </a>`,
}, },
false, false,
); );
return ` return `
<p> <p>
${extraCostParagraph} ${extraCostParagraph}
</p> </p>
...@@ -98,22 +104,25 @@ ...@@ -98,22 +104,25 @@
${externalIpParagraph} ${externalIpParagraph}
</p> </p>
`; `;
}, },
prometheusDescription() { prometheusDescription() {
return sprintf( return sprintf(
_.escape(s__( _.escape(
s__(
`ClusterIntegration|Prometheus is an open-source monitoring system `ClusterIntegration|Prometheus is an open-source monitoring system
with %{gitlabIntegrationLink} to monitor deployed applications.`, 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"> target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`, ${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`,
}, },
false, false,
); );
},
}, },
}; },
};
</script> </script>
<template> <template>
...@@ -205,7 +214,7 @@ ...@@ -205,7 +214,7 @@
> >
{{ s__(`ClusterIntegration|The IP address is in {{ s__(`ClusterIntegration|The IP address is in
the process of being assigned. Please check your Kubernetes 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 <a
:href="ingressHelpPath" :href="ingressHelpPath"
......
<script> <script>
import tooltip from '~/vue_shared/directives/tooltip'; 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 { pluralize } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
export default { export default {
components: { components: {
icon, Icon,
}, },
directives: { directives: {
tooltip, tooltip,
......
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants'; import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue'; import RadioGroup from './radio_group.vue';
export default { export default {
components: { components: {
RadioGroup, RadioGroup,
},
computed: {
...mapState(['currentBranchId']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
{ branchName: `<strong class="monospace">${this.currentBranchId}</strong>` },
false,
);
}, },
computed: { },
...mapState([ commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
'currentBranchId', commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
]), commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
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,
};
</script> </script>
<template> <template>
...@@ -53,13 +39,11 @@ ...@@ -53,13 +39,11 @@
:value="$options.commitToNewBranch" :value="$options.commitToNewBranch"
:label="__('Create a new branch')" :label="__('Create a new branch')"
:show-input="true" :show-input="true"
:help-text="commitToNewBranchText"
/> />
<radio-group <radio-group
:value="$options.commitToNewBranchMR" :value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')" :label="__('Create a new branch and merge request')"
:show-input="true" :show-input="true"
:help-text="newMergeRequestHelpText"
/> />
</div> </div>
</template> </template>
<script>
import { __, sprintf } from '../../../locale';
import Icon from '../../../vue_shared/components/icon.vue';
import popover from '../../../vue_shared/directives/popover';
import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
export default {
directives: {
popover,
},
components: {
Icon,
},
props: {
text: {
type: String,
required: true,
},
},
data() {
return {
scrollTop: 0,
isFocused: false,
};
},
computed: {
allLines() {
return this.text.split('\n').map((line, i) => ({
text: line.substr(0, this.getLineLength(i)) || ' ',
highlightedText: line.substr(this.getLineLength(i)),
}));
},
},
methods: {
handleScroll() {
if (this.$refs.textarea) {
this.$nextTick(() => {
this.scrollTop = this.$refs.textarea.scrollTop;
});
}
},
getLineLength(i) {
return i === 0 ? MAX_TITLE_LENGTH : MAX_BODY_LENGTH;
},
onInput(e) {
this.$emit('input', e.target.value);
},
updateIsFocused(isFocused) {
this.isFocused = isFocused;
},
},
popoverOptions: {
trigger: 'hover',
placement: 'top',
content: sprintf(
__(`
The character highligher helps you keep the subject line to %{titleLength} characters
and wrap the body at %{bodyLength} so they are readable in git.
`),
{ titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
),
},
};
</script>
<template>
<fieldset class="common-note-form ide-commit-message-field">
<div
class="md-area"
:class="{
'is-focused': isFocused
}"
>
<div
v-once
class="md-header"
>
<ul class="nav-links">
<li>
{{ __('Commit Message') }}
<span
v-popover="$options.popoverOptions"
class="help-block prepend-left-10"
>
<icon
name="question"
/>
</span>
</li>
</ul>
</div>
<div class="ide-commit-message-textarea-container">
<div class="ide-commit-message-highlights-container">
<div
class="note-textarea highlights monospace"
:style="{
transform: `translate3d(0, ${-scrollTop}px, 0)`
}"
>
<div
v-for="(line, index) in allLines"
:key="index"
>
<span
v-text="line.text"
>
</span><mark
v-show="line.highlightedText"
v-text="line.highlightedText"
>
</mark>
</div>
</div>
</div>
<textarea
class="note-textarea ide-commit-message-textarea"
name="commit-message"
:placeholder="__('Write a commit message...')"
:value="text"
@scroll="handleScroll"
@input="onInput"
@focus="updateIsFocused(true)"
@blur="updateIsFocused(false)"
ref="textarea"
>
</textarea>
</div>
</div>
</fieldset>
</template>
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
directives: { directives: {
tooltip, tooltip,
},
props: {
value: {
type: String,
required: true,
}, },
props: { label: {
value: { type: String,
type: String, required: false,
required: true, default: null,
},
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,
},
}, },
computed: { checked: {
...mapState('commit', [ type: Boolean,
'commitAction', required: false,
]), default: false,
...mapGetters('commit', [
'newBranchName',
]),
}, },
methods: { showInput: {
...mapActions('commit', [ type: Boolean,
'updateCommitAction', required: false,
'updateBranchName', default: false,
]),
}, },
}; },
computed: {
...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']),
},
methods: {
...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
},
};
</script> </script>
<template> <template>
...@@ -65,18 +53,6 @@ ...@@ -65,18 +53,6 @@
{{ label }} {{ label }}
</template> </template>
<slot v-else></slot> <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> </span>
</label> </label>
<div <div
...@@ -85,7 +61,7 @@ ...@@ -85,7 +61,7 @@
> >
<input <input
type="text" type="text"
class="form-control" class="form-control monospace"
:placeholder="newBranchName" :placeholder="newBranchName"
@input="updateBranchName($event.target.value)" @input="updateBranchName($event.target.value)"
/> />
......
...@@ -6,8 +6,9 @@ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; ...@@ -6,8 +6,9 @@ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import commitFilesList from './commit_sidebar/list.vue'; import commitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.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 * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue';
export default { export default {
components: { components: {
...@@ -17,6 +18,7 @@ export default { ...@@ -17,6 +18,7 @@ export default {
EmptyState, EmptyState,
Actions, Actions,
LoadingButton, LoadingButton,
CommitMessageField,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -92,16 +94,10 @@ export default { ...@@ -92,16 +94,10 @@ export default {
@submit.prevent.stop="commitChanges" @submit.prevent.stop="commitChanges"
v-if="!rightPanelCollapsed" v-if="!rightPanelCollapsed"
> >
<div class="multi-file-commit-fieldset"> <commit-message-field
<textarea :text="commitMessage"
class="form-control multi-file-commit-message" @input="updateCommitMessage"
name="commit-message" />
:value="commitMessage"
:placeholder="__('Write a commit message...')"
@input="updateCommitMessage($event.target.value)"
>
</textarea>
</div>
<div class="clearfix prepend-top-15"> <div class="clearfix prepend-top-15">
<actions /> <actions />
<loading-button <loading-button
......
// Fuzzy file finder
export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72;
...@@ -36,11 +36,11 @@ const router = new VueRouter({ ...@@ -36,11 +36,11 @@ const router = new VueRouter({
base: `${gon.relative_url_root}/-/ide/`, base: `${gon.relative_url_root}/-/ide/`,
routes: [ routes: [
{ {
path: '/project/:namespace/:project', path: '/project/:namespace/:project+',
component: EmptyRouterComponent, component: EmptyRouterComponent,
children: [ children: [
{ {
path: ':targetmode/:branch/*', path: ':targetmode(edit|tree|blob)/:branch/*',
component: EmptyRouterComponent, component: EmptyRouterComponent,
}, },
{ {
......
...@@ -5,45 +5,71 @@ import * as types from '../mutation_types'; ...@@ -5,45 +5,71 @@ import * as types from '../mutation_types';
export const getProjectData = ( export const getProjectData = (
{ commit, state, dispatch }, { commit, state, dispatch },
{ namespace, projectId, force = false } = {}, { namespace, projectId, force = false } = {},
) => new Promise((resolve, reject) => { ) =>
if (!state.projects[`${namespace}/${projectId}`] || force) { new Promise((resolve, reject) => {
commit(types.TOGGLE_LOADING, { entry: state }); if (!state.projects[`${namespace}/${projectId}`] || force) {
service.getProjectData(namespace, projectId)
.then(res => res.data)
.then((data) => {
commit(types.TOGGLE_LOADING, { entry: state }); commit(types.TOGGLE_LOADING, { entry: state });
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); service
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); .getProjectData(namespace, projectId)
resolve(data); .then(res => res.data)
}) .then(data => {
.catch(() => { commit(types.TOGGLE_LOADING, { entry: state });
flash('Error loading project data. Please try again.', 'alert', document, null, false, true); commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
reject(new Error(`Project not loaded ${namespace}/${projectId}`)); if (!state.currentProjectId)
}); commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
} else { resolve(data);
resolve(state.projects[`${namespace}/${projectId}`]); })
} .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 = ( export const getBranchData = (
{ commit, state, dispatch }, { commit, state, dispatch },
{ projectId, branchId, force = false } = {}, { projectId, branchId, force = false } = {},
) => new Promise((resolve, reject) => { ) =>
if ((typeof state.projects[`${projectId}`] === 'undefined' || new Promise((resolve, reject) => {
!state.projects[`${projectId}`].branches[branchId]) if (
|| force) { typeof state.projects[`${projectId}`] === 'undefined' ||
service.getBranchData(`${projectId}`, branchId) !state.projects[`${projectId}`].branches[branchId] ||
.then(({ data }) => { force
const { id } = data.commit; ) {
commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data }); service
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); .getBranchData(`${projectId}`, branchId)
resolve(data); .then(({ data }) => {
}) const { id } = data.commit;
.catch(() => { commit(types.SET_BRANCH, {
flash('Error loading branch data. Please try again.', 'alert', document, null, false, true); projectPath: `${projectId}`,
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); branchName: branchId,
}); branch: data,
} else { });
resolve(state.projects[`${projectId}`].branches[branchId]); 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 { ...@@ -17,12 +17,8 @@ export default {
}); });
}, },
[types.SET_DIRECTORY_DATA](state, { data, treePath }) { [types.SET_DIRECTORY_DATA](state, { data, treePath }) {
Object.assign(state, { Object.assign(state.trees[treePath], {
trees: Object.assign(state.trees, { tree: data,
[treePath]: {
tree: data,
},
}),
}); });
}, },
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) { [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
......
...@@ -19,7 +19,6 @@ import AjaxCache from '~/lib/utils/ajax_cache'; ...@@ -19,7 +19,6 @@ import AjaxCache from '~/lib/utils/ajax_cache';
import Vue from 'vue'; import Vue from 'vue';
import syntaxHighlight from '~/syntax_highlight'; import syntaxHighlight from '~/syntax_highlight';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility'; import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash'; import Flash from './flash';
...@@ -198,6 +197,8 @@ export default class Notes { ...@@ -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', this.loadLazyDiff);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff-retry-button', this.onClickRetryLazyLoad.bind(this));
// fetch notes when tab becomes visible // fetch notes when tab becomes visible
this.$wrapperEl.on('visibilitychange', this.visibilityChange); this.$wrapperEl.on('visibilitychange', this.visibilityChange);
// when issue status changes, we need to refresh data // when issue status changes, we need to refresh data
...@@ -244,6 +245,7 @@ export default class Notes { ...@@ -244,6 +245,7 @@ export default class Notes {
this.$wrapperEl.off('click', '.js-comment-resolve-button'); this.$wrapperEl.off('click', '.js-comment-resolve-button');
this.$wrapperEl.off('click', '.system-note-commit-list-toggler'); 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');
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-main-target-form');
this.$wrapperEl.off('ajax:success', '.js-discussion-note-form'); this.$wrapperEl.off('ajax:success', '.js-discussion-note-form');
this.$wrapperEl.off('ajax:complete', '.js-main-target-form'); this.$wrapperEl.off('ajax:complete', '.js-main-target-form');
...@@ -1431,16 +1433,15 @@ export default class Notes { ...@@ -1431,16 +1433,15 @@ export default class Notes {
syntaxHighlight(fileHolder); syntaxHighlight(fileHolder);
} }
static renderDiffError($container) { onClickRetryLazyLoad(e) {
$container.find('.line_content').html( const $retryButton = $(e.currentTarget);
$(`
<div class="nothing-here-block"> $retryButton.prop('disabled', true);
${__(
'Unable to load the diff.', return this.loadLazyDiff(e)
)} <a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>? .then(() => {
</div> $retryButton.prop('disabled', false);
`), });
);
} }
loadLazyDiff(e) { loadLazyDiff(e) {
...@@ -1449,20 +1450,35 @@ export default class Notes { ...@@ -1449,20 +1450,35 @@ export default class Notes {
$container.find('.js-toggle-lazy-diff').removeClass('js-toggle-lazy-diff'); $container.find('.js-toggle-lazy-diff').removeClass('js-toggle-lazy-diff');
const tableEl = $container.find('tbody'); const $tableEl = $container.find('tbody');
if (tableEl.length === 0) return; if ($tableEl.length === 0) return;
const fileHolder = $container.find('.file-holder'); const fileHolder = $container.find('.file-holder');
const url = fileHolder.data('linesPath'); 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) .get(url)
.then(({ data }) => { .then(({ data }) => {
// Reset state in case last request returned error
$successContainer.removeClass('hidden');
$errorContainer.addClass('hidden');
Notes.renderDiffContent($container, data); Notes.renderDiffContent($container, data);
}) })
.catch(() => { .catch(() => {
Notes.renderDiffError($container); $successContainer.addClass('hidden');
$errorContainer.removeClass('hidden');
}); });
}
return Promise.resolve();
} }
toggleCommitList(e) { toggleCommitList(e) {
......
...@@ -10,29 +10,25 @@ export default class PerformanceBarService { ...@@ -10,29 +10,25 @@ export default class PerformanceBarService {
} }
static registerInterceptor(peekUrl, callback) { static registerInterceptor(peekUrl, callback) {
vueResourceInterceptor = (request, next) => { const interceptor = response => {
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 requestId = response.headers['x-request-id']; 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); callback(requestId, requestUrl);
} }
return response; return response;
}); };
vueResourceInterceptor = (request, next) => next(interceptor);
Vue.http.interceptors.push(vueResourceInterceptor);
return axios.interceptors.response.use(interceptor);
} }
static removeInterceptor(interceptor) { static removeInterceptor(interceptor) {
......
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetPipelineBlocked',
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
</span>
</div>
</div>
`,
};
<script>
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'PipelineFailed',
components: {
statusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="warning"
:show-disabled-button="true"
/>
<div class="media-body space-children">
<span class="bold">
{{ s__(`mrWidget|The pipeline for this merge request failed.
Please retry the job or push a new commit to fix the failure`) }}
</span>
</div>
</div>
</template>
...@@ -31,7 +31,7 @@ export { default as ReadyToMergeState } from './components/states/ready_to_merge ...@@ -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 ShaMismatchState } from './components/states/sha_mismatch.vue';
export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.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 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 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 RebaseState } from './components/states/mr_widget_rebase.vue';
export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue'; export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue';
......
...@@ -175,7 +175,7 @@ ...@@ -175,7 +175,7 @@
</a> </a>
</span> </span>
<span v-else> <span v-else>
Cant find HEAD commit for this branch Can't find HEAD commit for this branch
</span> </span>
</div> </div>
</div> </div>
......
...@@ -503,3 +503,7 @@ fieldset[disabled] .btn, ...@@ -503,3 +503,7 @@ fieldset[disabled] .btn,
@extend %disabled; @extend %disabled;
} }
} }
.btn-no-padding {
padding: 0;
}
...@@ -180,10 +180,6 @@ ...@@ -180,10 +180,6 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
flex-grow: 1; flex-grow: 1;
.merge-request-branches & {
flex-direction: column;
}
} }
.commit-content { .commit-content {
......
...@@ -160,6 +160,11 @@ ...@@ -160,6 +160,11 @@
} }
} }
} }
.diff-loading-error-block {
padding: $gl-padding * 2 $gl-padding;
text-align: center;
}
} }
.image { .image {
......
...@@ -762,3 +762,20 @@ ...@@ -762,3 +762,20 @@
max-width: 100%; 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 @@ ...@@ -14,6 +14,11 @@
.commit-title { .commit-title {
margin: 0; margin: 0;
white-space: normal;
@media (max-width: $screen-sm-max) {
justify-content: flex-end;
}
} }
.ci-table { .ci-table {
......
...@@ -680,11 +680,6 @@ ...@@ -680,11 +680,6 @@
} }
} }
.multi-file-commit-message.form-control {
height: 160px;
resize: none;
}
.dirty-diff { .dirty-diff {
// !important need to override monaco inline style // !important need to override monaco inline style
width: 4px !important; width: 4px !important;
...@@ -874,3 +869,74 @@ ...@@ -874,3 +869,74 @@
align-items: center; align-items: center;
font-weight: $gl-font-weight-bold; 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 ...@@ -5,6 +5,7 @@ class ApplicationController < ActionController::Base
include Gitlab::GonHelper include Gitlab::GonHelper
include GitlabRoutingHelper include GitlabRoutingHelper
include PageLayoutHelper include PageLayoutHelper
include SafeParamsHelper
include SentryHelper include SentryHelper
include WorkhorseHelper include WorkhorseHelper
include EnforcesTwoFactorAuthentication include EnforcesTwoFactorAuthentication
......
...@@ -217,7 +217,7 @@ module NotesActions ...@@ -217,7 +217,7 @@ module NotesActions
def note_project def note_project
strong_memoize(:note_project) do strong_memoize(:note_project) do
return nil unless project next nil unless project
note_project_id = params[:note_project_id] note_project_id = params[:note_project_id]
...@@ -228,7 +228,7 @@ module NotesActions ...@@ -228,7 +228,7 @@ module NotesActions
project project
end 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 the_project
end end
......
...@@ -86,7 +86,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -86,7 +86,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
out_of_range = todos.current_page > total_pages out_of_range = todos.current_page > total_pages
if out_of_range 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 end
out_of_range out_of_range
......
...@@ -15,7 +15,7 @@ module Groups ...@@ -15,7 +15,7 @@ module Groups
def update def update
if @group.update(group_variables_params) if @group.update(group_variables_params)
respond_to do |format| respond_to do |format|
format.json { return render_group_variables } format.json { render_group_variables }
end end
else else
respond_to do |format| respond_to do |format|
......
...@@ -189,6 +189,6 @@ class GroupsController < Groups::ApplicationController ...@@ -189,6 +189,6 @@ class GroupsController < Groups::ApplicationController
params[:id] = group.to_param params[:id] = group.to_param
url_for(params) url_for(safe_params)
end end
end end
...@@ -60,13 +60,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -60,13 +60,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end end
format.patch do 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 send_git_patch @project.repository, @merge_request.diff_refs
end end
format.diff do 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 send_git_diff @project.repository, @merge_request.diff_refs
end end
......
...@@ -12,7 +12,7 @@ class Projects::VariablesController < Projects::ApplicationController ...@@ -12,7 +12,7 @@ class Projects::VariablesController < Projects::ApplicationController
def update def update
if @project.update(variables_params) if @project.update(variables_params)
respond_to do |format| respond_to do |format|
format.json { return render_variables } format.json { render_variables }
end end
else else
respond_to do |format| respond_to do |format|
......
...@@ -404,7 +404,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -404,7 +404,7 @@ class ProjectsController < Projects::ApplicationController
params[:namespace_id] = project.namespace.to_param params[:namespace_id] = project.namespace.to_param
params[:id] = project.to_param params[:id] = project.to_param
url_for(params) url_for(safe_params)
end end
def project_export_enabled def project_export_enabled
......
...@@ -146,6 +146,6 @@ class UsersController < ApplicationController ...@@ -146,6 +146,6 @@ class UsersController < ApplicationController
end end
def build_canonical_path(user) def build_canonical_path(user)
url_for(params.merge(username: user.to_param)) url_for(safe_params.merge(username: user.to_param))
end end
end end
...@@ -259,7 +259,7 @@ module BlobHelper ...@@ -259,7 +259,7 @@ module BlobHelper
options = [] options = []
if error == :collapsed 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 end
# If the error is `:server_side_but_stored_externally`, the simple viewer will show the same error, # If the error is `:server_side_but_stored_externally`, the simple viewer will show the same error,
......
...@@ -180,7 +180,7 @@ module DiffHelper ...@@ -180,7 +180,7 @@ module DiffHelper
private private
def diff_btn(title, name, selected) def diff_btn(title, name, selected)
params_copy = params.dup params_copy = safe_params.dup
params_copy[:view] = name params_copy[:view] = name
# Always use HTML to handle case where JSON diff rendered this button # Always use HTML to handle case where JSON diff rendered this button
......
module SafeParamsHelper
# Rails 5.0 requires to permit `params` if they're used in url helpers.
# Use this helper when generating links with `params.merge(...)`
def safe_params
if params.respond_to?(:permit!)
params.except(:host, :port, :protocol).permit!
else
params
end
end
end
...@@ -90,7 +90,7 @@ module TreeHelper ...@@ -90,7 +90,7 @@ module TreeHelper
end end
def commit_in_single_accessible_branch 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 "\ message = _("Your changes can be committed to %{branch_name} because a merge "\
"request is open.") % { branch_name: "<strong>#{branch_name}</strong>" } "request is open.") % { branch_name: "<strong>#{branch_name}</strong>" }
......
...@@ -6,6 +6,12 @@ module Emails ...@@ -6,6 +6,12 @@ module Emails
mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason)) mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason))
end 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) def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
......
...@@ -162,7 +162,7 @@ module Ci ...@@ -162,7 +162,7 @@ module Ci
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies') build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
end end
before_transition pending: :running do |build| after_transition pending: :running do |build|
build.ensure_metadata.update_timeout_state build.ensure_metadata.update_timeout_state
end end
end end
...@@ -479,7 +479,7 @@ module Ci ...@@ -479,7 +479,7 @@ module Ci
def user_variables def user_variables
Gitlab::Ci::Variables::Collection.new.tap do |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_ID', value: user.id.to_s)
variables.append(key: 'GITLAB_USER_EMAIL', value: user.email) variables.append(key: 'GITLAB_USER_EMAIL', value: user.email)
...@@ -594,7 +594,7 @@ module Ci ...@@ -594,7 +594,7 @@ module Ci
def persisted_variables def persisted_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables| Gitlab::Ci::Variables::Collection.new.tap do |variables|
return variables unless persisted? break variables unless persisted?
variables variables
.append(key: 'CI_JOB_ID', value: id.to_s) .append(key: 'CI_JOB_ID', value: id.to_s)
...@@ -643,7 +643,7 @@ module Ci ...@@ -643,7 +643,7 @@ module Ci
def persisted_environment_variables def persisted_environment_variables
Gitlab::Ci::Variables::Collection.new.tap do |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) variables.concat(persisted_environment.predefined_variables)
......
...@@ -4,7 +4,7 @@ module Ci ...@@ -4,7 +4,7 @@ module Ci
include HasVariable include HasVariable
include Presentable include Presentable
belongs_to :group belongs_to :group, class_name: "::Group"
alias_attribute :secret_value, :value alias_attribute :secret_value, :value
......
...@@ -87,7 +87,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -87,7 +87,7 @@ class CommitStatus < ActiveRecord::Base
transition [:created, :pending, :running, :manual] => :canceled transition [:created, :pending, :running, :manual] => :canceled
end end
before_transition created: [:pending, :running] do |commit_status| before_transition [:created, :skipped, :manual] => :pending do |commit_status|
commit_status.queued_at = Time.now commit_status.queued_at = Time.now
end end
......
...@@ -11,7 +11,9 @@ module CacheMarkdownField ...@@ -11,7 +11,9 @@ module CacheMarkdownField
extend ActiveSupport::Concern extend ActiveSupport::Concern
# Increment this number every time the renderer changes its output # 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 # changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze INVALIDATED_BY = %w[author project].freeze
...@@ -49,12 +51,14 @@ module CacheMarkdownField ...@@ -49,12 +51,14 @@ module CacheMarkdownField
# Always include a project key, or Banzai complains # Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project) 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) context = cached_markdown_fields[field].merge(project: project, group: group)
# Banzai is less strict about authors, so don't always have an author key # Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author) context[:author] = self.author if self.respond_to?(:author)
context[:markdown_engine] = markdown_engine
context context
end end
...@@ -69,7 +73,7 @@ module CacheMarkdownField ...@@ -69,7 +73,7 @@ module CacheMarkdownField
Banzai::Renderer.cacheless_render_field(self, markdown_field, options) Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
] ]
end.to_h 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) } updates.each {|html_field, data| write_attribute(html_field, data) }
end end
...@@ -90,7 +94,7 @@ module CacheMarkdownField ...@@ -90,7 +94,7 @@ module CacheMarkdownField
markdown_changed = attribute_changed?(markdown_field) || false markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_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) (html_changed || markdown_changed == html_changed)
end end
...@@ -109,6 +113,24 @@ module CacheMarkdownField ...@@ -109,6 +113,24 @@ module CacheMarkdownField
__send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend __send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend
end 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 included do
cattr_reader :cached_markdown_fields do cattr_reader :cached_markdown_fields do
FieldData.new FieldData.new
......
...@@ -49,6 +49,7 @@ class Issue < ActiveRecord::Base ...@@ -49,6 +49,7 @@ class Issue < ActiveRecord::Base
scope :without_due_date, -> { where(due_date: nil) } scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) } 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_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_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') } scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
......
...@@ -83,14 +83,14 @@ class NotificationRecipient ...@@ -83,14 +83,14 @@ class NotificationRecipient
def has_access? def has_access?
DeclarativePolicy.subject_scope do DeclarativePolicy.subject_scope do
return false unless user.can?(:receive_notifications) break false unless user.can?(:receive_notifications)
return true if @skip_read_ability break true if @skip_read_ability
return false if @target && !user.can?(:read_cross_project) break false if @target && !user.can?(:read_cross_project)
return false if @project && !user.can?(:read_project, @project) break false if @project && !user.can?(:read_project, @project)
return true unless read_ability break true unless read_ability
return true unless DeclarativePolicy.has_policy?(@target) break true unless DeclarativePolicy.has_policy?(@target)
user.can?(read_ability, @target) user.can?(read_ability, @target)
end end
......
...@@ -47,7 +47,8 @@ class NotificationSetting < ActiveRecord::Base ...@@ -47,7 +47,8 @@ class NotificationSetting < ActiveRecord::Base
].freeze ].freeze
EXCLUDED_WATCHER_EVENTS = [ EXCLUDED_WATCHER_EVENTS = [
:push_to_merge_request :push_to_merge_request,
:issue_due
].push(*EXCLUDED_PARTICIPATING_EVENTS).freeze ].push(*EXCLUDED_PARTICIPATING_EVENTS).freeze
def self.find_or_create_for(source) def self.find_or_create_for(source)
......
...@@ -1637,7 +1637,7 @@ class Project < ActiveRecord::Base ...@@ -1637,7 +1637,7 @@ class Project < ActiveRecord::Base
def container_registry_variables def container_registry_variables
Gitlab::Ci::Variables::Collection.new.tap do |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) variables.append(key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port)
......
...@@ -331,6 +331,7 @@ class Repository ...@@ -331,6 +331,7 @@ class Repository
return unless empty? return unless empty?
expire_method_caches(%i(has_visible_content?)) expire_method_caches(%i(has_visible_content?))
raw_repository.expire_has_local_branches_cache
end end
def lookup_cache def lookup_cache
......
...@@ -4,6 +4,9 @@ module Ci ...@@ -4,6 +4,9 @@ module Ci
class RegisterJobService class RegisterJobService
attr_reader :runner 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?) Result = Struct.new(:build, :valid?)
def initialize(runner) def initialize(runner)
...@@ -30,7 +33,7 @@ module Ci ...@@ -30,7 +33,7 @@ module Ci
end end
end end
builds.find do |build| builds.auto_include(false).find do |build|
next unless runner.can_pick?(build) next unless runner.can_pick?(build)
begin begin
...@@ -41,7 +44,7 @@ module Ci ...@@ -41,7 +44,7 @@ module Ci
build.run! build.run!
register_success(build) register_success(build)
return Result.new(build, true) return Result.new(build, true) # rubocop:disable Cop/AvoidReturnFromBlocks
rescue Ci::Build::MissingDependenciesError rescue Ci::Build::MissingDependenciesError
build.drop!(:missing_dependency_failure) build.drop!(:missing_dependency_failure)
end end
...@@ -104,10 +107,22 @@ module Ci ...@@ -104,10 +107,22 @@ module Ci
end end
def register_success(job) 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 attempt_counter.increment
end 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 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") @failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job")
end end
...@@ -117,7 +132,7 @@ module Ci ...@@ -117,7 +132,7 @@ module Ci
end end
def job_queue_duration_seconds 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 end
end end
...@@ -13,7 +13,7 @@ module Clusters ...@@ -13,7 +13,7 @@ module Clusters
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
rescue ActiveRecord::RecordInvalid => e 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 end
private private
......
...@@ -17,7 +17,7 @@ module Clusters ...@@ -17,7 +17,7 @@ module Clusters
when 'DONE' when 'DONE'
finalize_creation finalize_creation
else 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 end
end end
......
...@@ -19,8 +19,8 @@ class CreateDeploymentService ...@@ -19,8 +19,8 @@ class CreateDeploymentService
environment.fire_state_event(action) environment.fire_state_event(action)
return unless environment.save break unless environment.save
return if environment.stopped? break if environment.stopped?
deploy.tap(&:update_merge_request_metrics!) deploy.tap(&:update_merge_request_metrics!)
end end
......
...@@ -10,7 +10,7 @@ class ImportExportCleanUpService ...@@ -10,7 +10,7 @@ class ImportExportCleanUpService
def execute def execute
Gitlab::Metrics.measure(:import_export_clean_up) do Gitlab::Metrics.measure(:import_export_clean_up) do
return unless File.directory?(path) next unless File.directory?(path)
clean_up_export_files clean_up_export_files
end end
......
...@@ -203,10 +203,11 @@ module NotificationRecipientService ...@@ -203,10 +203,11 @@ module NotificationRecipientService
attr_reader :action attr_reader :action
attr_reader :previous_assignee attr_reader :previous_assignee
attr_reader :skip_current_user 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 @target = target
@current_user = current_user @current_user = current_user
@action = action @action = action
@custom_action = custom_action
@previous_assignee = previous_assignee @previous_assignee = previous_assignee
@skip_current_user = skip_current_user @skip_current_user = skip_current_user
end end
...@@ -236,7 +237,13 @@ module NotificationRecipientService ...@@ -236,7 +237,13 @@ module NotificationRecipientService
add_mentions(current_user, target: target) add_mentions(current_user, target: target)
# Add the assigned users, if any # 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 # We use the `:participating` notification level in order to match existing legacy behavior as captured
# in existing specs (notification_service_spec.rb ~ line 507) # in existing specs (notification_service_spec.rb ~ line 507)
add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees
......
...@@ -373,6 +373,20 @@ class NotificationService ...@@ -373,6 +373,20 @@ class NotificationService
end end
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 protected
def new_resource_email(target, method) def new_resource_email(target, method)
......
...@@ -137,7 +137,7 @@ module Projects ...@@ -137,7 +137,7 @@ module Projects
return true unless Gitlab.config.registry.enabled return true unless Gitlab.config.registry.enabled
ContainerRepository.build_root_repository(project).tap do |repository| 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
end end
......
...@@ -10,7 +10,7 @@ class RepositoryArchiveCleanUpService ...@@ -10,7 +10,7 @@ class RepositoryArchiveCleanUpService
def execute def execute
Gitlab::Metrics.measure(:repository_archive_clean_up) do Gitlab::Metrics.measure(:repository_archive_clean_up) do
return unless File.directory?(path) next unless File.directory?(path)
clean_up_old_archives clean_up_old_archives
clean_up_empty_directories clean_up_empty_directories
......
...@@ -159,7 +159,7 @@ module SystemNoteService ...@@ -159,7 +159,7 @@ module SystemNoteService
body = if noteable.time_estimate == 0 body = if noteable.time_estimate == 0
"removed time estimate" "removed time estimate"
else else
"changed time estimate to #{parsed_time}" "changed time estimate to #{parsed_time},"
end end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking')) create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
......
...@@ -19,7 +19,7 @@ module TestHooks ...@@ -19,7 +19,7 @@ module TestHooks
error_message = catch(:validation_error) do error_message = catch(:validation_error) do
sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend 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 end
error(error_message) error(error_message)
......
xml.title "#{current_user.name} issues" 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.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url xml.id issues_dashboard_url
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
- page_title _("Issues") - page_title _("Issues")
- @breadcrumb_link = issues_dashboard_path(assignee_id: current_user.id) - @breadcrumb_link = issues_dashboard_path(assignee_id: current_user.id)
= content_for :meta_tags do = 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 .top-area
= render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set = render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set
.nav-controls .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') = icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- unless expanded - unless expanded
- diff_data = { lines_path: project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion) } - 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 .js-file-title.file-title.file-title-flex-parent
.file-header-content .file-header-content
= render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false = render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false
...@@ -28,8 +28,11 @@ ...@@ -28,8 +28,11 @@
%tr.line_holder.line-holder-placeholder %tr.line_holder.line-holder-placeholder
%td.old_line.diff-line-num %td.old_line.diff-line-num
%td.new_line.diff-line-num %td.new_line.diff-line-num
%td.line_content %td.line_content.js-success-lazy-load
.js-code-placeholder .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 = render "discussions/diff_discussion", discussions: [discussion], expanded: true
- else - else
- partial = (diff_file.new_file? || diff_file.deleted_file?) ? 'single_image_diff' : 'replaced_image_diff' - partial = (diff_file.new_file? || diff_file.deleted_file?) ? 'single_image_diff' : 'replaced_image_diff'
......
xml.title "#{@group.name} issues" 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.link href: issues_group_url, rel: "alternate", type: "text/html"
xml.id issues_group_url xml.id issues_group_url
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
......
- page_title "Issues" - page_title "Issues"
= content_for :meta_tags do = 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? - if group_issues_count(state: 'all').zero?
= render 'shared/empty_states/issues', project_select_button: true = render 'shared/empty_states/issues', project_select_button: true
......
%p.details
#{link_to @issue.author_name, user_url(@issue.author)}'s issue is due soon.
- if @issue.assignees.any?
%p
Assignee: #{@issue.assignee_list}
%p
This issue is due on: #{@issue.due_date.to_s(:medium)}
- if @issue.description
%div
= markdown(@issue.description, pipeline: :email, author: @issue.author)
The following issue is due on <%= @issue.due_date %>:
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= @issue.author_name %>
Assignee: <%= @issue.assignee_list %>
<%= @issue.description %>
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?) - load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?)
- external_embed = local_assigns.fetch(:external_embed, false) - 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) } .blob-viewer{ data: { type: viewer.type, rich_type: rich_type, url: viewer_url }, class: ('hidden' if hidden) }
- if render_error - if render_error
= render 'projects/blob/render_error', viewer: viewer = render 'projects/blob/render_error', viewer: viewer
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
= s_('Branches|Cant find HEAD commit for this branch') = s_('Branches|Cant find HEAD commit for this branch')
- if branch.name != @repository.root_ref - 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, default_branch: @repository.root_ref,
number_commits_ahead: diverging_count_label(number_commits_ahead) } } number_commits_ahead: diverging_count_label(number_commits_ahead) } }
.graph-side .graph-side
......
...@@ -8,6 +8,6 @@ ...@@ -8,6 +8,6 @@
%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration') %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') %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') %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' = 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 @@ ...@@ -22,7 +22,7 @@
= author_avatar(commit, size: 36) = author_avatar(commit, size: 36)
.commit-detail.flex-list .commit-detail.flex-list
.commit-content .commit-content.qa-commit-content
- if view_details && merge_request - 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" = link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title"
- else - else
......
- diff_file = viewer.diff_file - 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 } } .nothing-here-block.diff-collapsed{ data: { diff_for_path: url } }
This diff is collapsed. This diff is collapsed.
%a.click-to-expand Click to expand it. %a.click-to-expand Click to expand it.
= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do = link_to safe_params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss') = icon('rss')
- if @can_bulk_update - if @can_bulk_update
= button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
......
xml.title "#{@project.name} issues" 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.link href: project_issues_url(@project), rel: "alternate", type: "text/html"
xml.id project_issues_url(@project) xml.id project_issues_url(@project)
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- new_issue_email = @project.new_issuable_address(current_user, 'issue') - new_issue_email = @project.new_issuable_address(current_user, 'issue')
= content_for :meta_tags do = 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? - if project_issues(@project).exists?
%div{ class: (container_class) } %div{ class: (container_class) }
......
...@@ -3,7 +3,7 @@ ...@@ -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| = 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 .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 .col-md-6
.panel.panel-default.panel-new-merge-request .panel.panel-default.panel-new-merge-request
.panel-heading .panel-heading
......
...@@ -26,16 +26,16 @@ ...@@ -26,16 +26,16 @@
- else - else
%ul.merge-request-tabs.nav-links.no-top.no-bottom %ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.commits-tab.active %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 Commits
%span.badge= @commits.size %span.badge= @commits.size
- if @pipelines.any? - if @pipelines.any?
%li.builds-tab %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 Pipelines
%span.badge= @pipelines.size %span.badge= @pipelines.size
%li.diffs-tab %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 Changes
%span.badge= @merge_request.diff_size %span.badge= @merge_request.diff_size
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
-# This tab is always loaded via AJAX -# This tab is always loaded via AJAX
- if @pipelines.any? - if @pipelines.any?
#pipelines.pipelines.tab-pane #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 .mr-loading-status
= spinner = spinner
- breadcrumb_title "Pipelines" - breadcrumb_title "Pipelines"
- page_title "New Pipeline" - page_title = s_("Pipeline|Run Pipeline")
%h3.page-title %h3.page-title
New Pipeline = s_("Pipeline|Run Pipeline")
%hr %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_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_errors(@pipeline)
.form-group .form-group
= f.label :ref, 'Create for', class: 'control-label' = f.label :ref, s_('Pipeline|Run on'), class: 'control-label'
.col-sm-10 .col-sm-10
= hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch, = dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide git-revision-dropdown-toggle', 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]' } }) 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 .form-actions
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3 = f.submit s_('Pipeline|Run pipeline'), class: 'btn btn-success', tabindex: 3
= link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-cancel' = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default pull-right'
-# haml-lint:disable InlineJavaScript -# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe %script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
- content_for :push_access_levels do - content_for :push_access_levels do
.push_access_levels-container .push_access_levels-container
= dropdown_tag('Select', = dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-push wide', options: { toggle_class: 'js-allowed-to-push qa-allowed-to-push-select wide',
dropdown_class: 'dropdown-menu-selectable capitalize-header', 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' }}) 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' = render 'projects/protected_branches/shared/create_protected_branch'
...@@ -6,5 +6,5 @@ ...@@ -6,5 +6,5 @@
%td %td
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level = 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') , = 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 }}) data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: protected_branch.push_access_levels.first.id }})
.protected-branches-list.js-protected-branches-list .protected-branches-list.js-protected-branches-list.qa-protected-branches-list
- if @protected_branches.empty? - if @protected_branches.empty?
.panel-heading .panel-heading
%h3.panel-title %h3.panel-title
......
= f.hidden_field(:name) = f.hidden_field(:name)
= dropdown_tag('Select branch or create wildcard', = dropdown_tag('Select branch or create wildcard',
options: { toggle_class: 'js-protected-branch-select js-filter-submit wide git-revision-dropdown-toggle', 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", placeholder: "Search protected branches", filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown qa-protected-branch-dropdown", placeholder: "Search protected branches",
footer_content: true, footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true, data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_branch_name], selected: params[:protected_branch_name],
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.settings-header .settings-header
%h4 %h4
Protected Branches Protected Branches
%button.btn.js-settings-toggle{ type: 'button' } %button.btn.js-settings-toggle.qa-expand-protected-branches{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Keep stable branches secure and force developers to use merge requests. Keep stable branches secure and force developers to use merge requests.
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) } } %tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) } }
%td %td
%span.ref-name= protected_branch.name %span.ref-name.qa-protected-branch-name= protected_branch.name
- if @project.root_ref?(protected_branch.name) - if @project.root_ref?(protected_branch.name)
%span.label.label-info.prepend-left-5 default %span.label.label-info.prepend-left-5 default
......
...@@ -8,8 +8,8 @@ ...@@ -8,8 +8,8 @@
- @options && @options.each do |key, value| - @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil = hidden_field_tag key, value, id: nil
.dropdown .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_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{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } .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-page-one
= dropdown_title _("Switch branch/tag") = dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags") = dropdown_filter _("Search branches and tags")
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
- cronjob:stuck_import_jobs - cronjob:stuck_import_jobs
- cronjob:stuck_merge_jobs - cronjob:stuck_merge_jobs
- cronjob:trending_projects - cronjob:trending_projects
- cronjob:issue_due_scheduler
- gcp_cluster:cluster_install_app - gcp_cluster:cluster_install_app
- gcp_cluster:cluster_provision - gcp_cluster:cluster_provision
...@@ -39,6 +40,8 @@ ...@@ -39,6 +40,8 @@
- github_importer:github_import_stage_import_pull_requests - github_importer:github_import_stage_import_pull_requests
- github_importer:github_import_stage_import_repository - github_importer:github_import_stage_import_repository
- mail_scheduler:mail_scheduler_issue_due
- object_storage_upload - object_storage_upload
- object_storage:object_storage_background_move - object_storage:object_storage_background_move
- object_storage:object_storage_migrate_uploads - object_storage:object_storage_migrate_uploads
......
module MailSchedulerQueue
extend ActiveSupport::Concern
included do
queue_namespace :mail_scheduler
end
end
class IssueDueSchedulerWorker
include ApplicationWorker
include CronjobQueue
def perform
project_ids = Issue.opened.due_tomorrow.group(:project_id).pluck(:project_id).map { |id| [id] }
MailScheduler::IssueDueWorker.bulk_perform_async(project_ids)
end
end
module MailScheduler
class IssueDueWorker
include ApplicationWorker
include MailSchedulerQueue
def perform(project_id)
notification_service = NotificationService.new
Issue.opened.due_tomorrow.in_projects(project_id).preload(:project).find_each do |issue|
notification_service.issue_due(issue)
end
end
end
end
...@@ -33,7 +33,7 @@ class PostReceive ...@@ -33,7 +33,7 @@ class PostReceive
unless @user unless @user
log("Triggered hook for non-existing user \"#{post_received.identifier}\"") log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
return false return false # rubocop:disable Cop/AvoidReturnFromBlocks
end end
if Gitlab::Git.tag_ref?(ref) if Gitlab::Git.tag_ref?(ref)
......
...@@ -38,7 +38,7 @@ class StuckCiJobsWorker ...@@ -38,7 +38,7 @@ class StuckCiJobsWorker
def drop_stuck(status, timeout) def drop_stuck(status, timeout)
search(status, timeout) do |build| search(status, timeout) do |build|
return unless build.stuck? break unless build.stuck?
drop_build :stuck, build, status, timeout drop_build :stuck, build, status, timeout
end end
......
#!/usr/bin/env ruby
require 'optparse'
require 'open3'
require 'rainbow/refinement'
using Rainbow
BRANCH_PREFIX = 'security'.freeze
STABLE_BRANCH_SUFFIX = 'stable'.freeze
REMOTE = 'dev'.freeze
options = { version: nil, branch: nil, sha: nil }
parser = OptionParser.new do |opts|
opts.banner = "Usage: #{$0} [options]"
opts.on('-v', '--version 10.0', 'Version') do |version|
options[:version] = version&.tr('.', '-')
end
opts.on('-b', '--branch security-fix-branch', 'Original branch name') do |branch|
options[:branch] = branch
end
opts.on('-s', '--sha abcd', 'SHA to cherry pick') do |sha|
options[:sha] = sha
end
opts.on('-h', '--help', 'Displays Help') do
puts opts
exit
end
end
parser.parse!
abort("Missing options. Use #{$0} --help to see the list of options available".red) if options.values.include?(nil)
abort("Wrong version format #{options[:version].bold}".red) unless options[:version] =~ /\A\d*\-\d*\Z/
branch = [BRANCH_PREFIX, options[:branch], options[:version]].join('-').freeze
stable_branch = "#{options[:version]}-#{STABLE_BRANCH_SUFFIX}".freeze
command = "git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}"
_stdin, stdout, stderr = Open3.popen3(command)
puts stdout.read&.green
puts stderr.read&.red
---
title: Add cron job to email users on issue due date
merge_request: 17985
author: Stuart Nelson
type: added
---
title: Improves wording in new pipeline page
merge_request:
author:
type: other
---
title: Add a comma to the time estimate system notes
merge_request: 18326
author:
type: changed
---
title: Remove ahead/behind graphs on project branches on mobile
merge_request: 18415
author: Takuya Noguchi
type: other
---
title: Rubocop rule to avoid returning from a block
merge_request: 18000
author: Jacopo Beschi @jacopo-beschi
type: added
---
title: Breaks commit not found message in pipelines table
merge_request:
author:
type: fixed
---
title: Replace GKE acronym with Google Kubernetes Engine
merge_request:
author:
type: other
---
title: Fix `Trace::HttpIO` can not render multi-byte chars
merge_request: 18417
author:
type: fixed
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment