Commit 64be67f3 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into ide-file-finder

parents 48fb30f7 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
...@@ -45,4 +45,4 @@ When removing columns, tables, indexes or other structures: ...@@ -45,4 +45,4 @@ When removing columns, tables, indexes or other structures:
- [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) - [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
- [ ] Internationalization required/considered - [ ] Internationalization required/considered
- [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan - [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan
- [ ] End-to-end tests pass (`package-qa` manual pipeline job) - [ ] End-to-end tests pass (`package-and-qa` manual pipeline job)
...@@ -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)
...@@ -206,7 +206,7 @@ GEM ...@@ -206,7 +206,7 @@ GEM
railties (>= 3.0.0) railties (>= 3.0.0)
faraday (0.12.2) faraday (0.12.2)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
faraday_middleware (0.11.0.1) faraday_middleware (0.12.2)
faraday (>= 0.7.4, < 1.0) faraday (>= 0.7.4, < 1.0)
faraday_middleware-multi_json (0.0.6) faraday_middleware-multi_json (0.0.6)
faraday_middleware faraday_middleware
...@@ -590,7 +590,7 @@ GEM ...@@ -590,7 +590,7 @@ GEM
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (0.9.6) os (0.9.6)
parallel (1.12.1) parallel (1.12.1)
parser (2.5.0.5) parser (2.5.1.0)
ast (~> 2.4.0) ast (~> 2.4.0)
parslet (1.5.0) parslet (1.5.0)
blankslate (~> 2.0) blankslate (~> 2.0)
...@@ -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,
...@@ -43,10 +40,13 @@ ...@@ -43,10 +40,13 @@
computed: { computed: {
generalApplicationDescription() { generalApplicationDescription() {
return sprintf( return sprintf(
_.escape(s__( _.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>`,
...@@ -65,12 +65,15 @@ ...@@ -65,12 +65,15 @@
}, },
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>`, boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer"> 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>`,
...@@ -79,10 +82,13 @@ ...@@ -79,10 +82,13 @@
); );
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>`,
...@@ -101,10 +107,13 @@ ...@@ -101,10 +107,13 @@
}, },
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>`,
...@@ -113,7 +122,7 @@ ...@@ -113,7 +122,7 @@
); );
}, },
}, },
}; };
</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"
......
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore';
import { import {
getSelector, getSelector,
togglePopover,
inserted, inserted,
mouseenter,
mouseleave,
} from './feature_highlight_helper'; } from './feature_highlight_helper';
import {
togglePopover,
mouseenter,
debouncedMouseleave,
} from '../shared/popover';
export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
const $selector = $(getSelector(id)); const $selector = $(getSelector(id));
const $parent = $selector.parent(); const $parent = $selector.parent();
const $popoverContent = $parent.siblings('.feature-highlight-popover-content'); const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
const hideOnScroll = togglePopover.bind($selector, false); const hideOnScroll = togglePopover.bind($selector, false);
const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
$selector $selector
// Setup popover // Setup popover
...@@ -29,13 +29,10 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { ...@@ -29,13 +29,10 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
`, `,
}) })
.on('mouseenter', mouseenter) .on('mouseenter', mouseenter)
.on('mouseleave', debouncedMouseleave) .on('mouseleave', debouncedMouseleave(debounceTimeout))
.on('inserted.bs.popover', inserted) .on('inserted.bs.popover', inserted)
.on('show.bs.popover', () => { .on('show.bs.popover', () => {
window.addEventListener('scroll', hideOnScroll); window.addEventListener('scroll', hideOnScroll, { once: true });
})
.on('hide.bs.popover', () => {
window.removeEventListener('scroll', hideOnScroll);
}) })
// Display feature highlight // Display feature highlight
.removeAttr('disabled'); .removeAttr('disabled');
......
...@@ -3,20 +3,10 @@ import axios from '../lib/utils/axios_utils'; ...@@ -3,20 +3,10 @@ import axios from '../lib/utils/axios_utils';
import { __ } from '../locale'; import { __ } from '../locale';
import Flash from '../flash'; import Flash from '../flash';
import LazyLoader from '../lazy_loader'; import LazyLoader from '../lazy_loader';
import { togglePopover } from '../shared/popover';
export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`; export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
export function togglePopover(show) {
const isAlreadyShown = this.hasClass('js-popover-show');
if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) {
return false;
}
this.popover(show ? 'show' : 'hide');
this.toggleClass('disable-animation js-popover-show', show);
return true;
}
export function dismiss(highlightId) { export function dismiss(highlightId) {
axios.post(this.attr('data-dismiss-endpoint'), { axios.post(this.attr('data-dismiss-endpoint'), {
feature_name: highlightId, feature_name: highlightId,
...@@ -27,23 +17,6 @@ export function dismiss(highlightId) { ...@@ -27,23 +17,6 @@ export function dismiss(highlightId) {
this.hide(); this.hide();
} }
export function mouseleave() {
if (!$('.popover:hover').length > 0) {
const $featureHighlight = $(this);
togglePopover.call($featureHighlight, false);
}
}
export function mouseenter() {
const $featureHighlight = $(this);
const showedPopover = togglePopover.call($featureHighlight, true);
if (showedPopover) {
$('.popover')
.on('mouseleave', mouseleave.bind($featureHighlight));
}
}
export function inserted() { export function inserted() {
const popoverId = this.getAttribute('aria-describedby'); const popoverId = this.getAttribute('aria-describedby');
const highlightId = this.dataset.highlight; const highlightId = this.dataset.highlight;
......
<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: { computed: {
...mapState([ ...mapState(['currentBranchId']),
'currentBranchId',
]),
newMergeRequestHelpText() {
return sprintf(
__('Creates a new branch from %{branchName} and re-directs to create a new merge request'),
{ branchName: this.currentBranchId },
);
},
commitToCurrentBranchText() { commitToCurrentBranchText() {
return sprintf( return sprintf(
__('Commit to %{branchName} branch'), __('Commit to %{branchName} branch'),
{ branchName: `<strong>${this.currentBranchId}</strong>` }, { branchName: `<strong class="monospace">${this.currentBranchId}</strong>` },
false, false,
); );
}, },
commitToNewBranchText() {
return sprintf(
__('Creates a new branch from %{branchName}'),
{ branchName: this.currentBranchId },
);
},
}, },
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH, commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH, commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR, 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,
}, },
...@@ -26,27 +26,15 @@ ...@@ -26,27 +26,15 @@
required: false, required: false,
default: false, default: false,
}, },
helpText: {
type: String,
required: false,
default: null,
},
}, },
computed: { computed: {
...mapState('commit', [ ...mapState('commit', ['commitAction']),
'commitAction', ...mapGetters('commit', ['newBranchName']),
]),
...mapGetters('commit', [
'newBranchName',
]),
}, },
methods: { methods: {
...mapActions('commit', [ ...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
'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)"
/> />
......
...@@ -5,6 +5,7 @@ import icon from '~/vue_shared/components/icon.vue'; ...@@ -5,6 +5,7 @@ import icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; 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 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'; import Actions from './commit_sidebar/actions.vue';
...@@ -15,6 +16,7 @@ export default { ...@@ -15,6 +16,7 @@ export default {
commitFilesList, commitFilesList,
Actions, Actions,
LoadingButton, LoadingButton,
CommitMessageField,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -38,15 +40,9 @@ export default { ...@@ -38,15 +40,9 @@ export default {
'changedFiles', 'changedFiles',
]), ]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']), ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters('commit', [ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']),
'commitButtonDisabled',
'discardDraftButtonDisabled',
'branchName',
]),
statusSvg() { statusSvg() {
return this.lastCommitMsg return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
? this.committedStateSvgPath
: this.noChangesStateSvgPath;
}, },
}, },
methods: { methods: {
...@@ -64,9 +60,7 @@ export default { ...@@ -64,9 +60,7 @@ export default {
}); });
}, },
forceCreateNewBranch() { forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
this.commitChanges(),
);
}, },
}, },
}; };
...@@ -105,16 +99,10 @@ export default { ...@@ -105,16 +99,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
......
...@@ -6,3 +6,7 @@ export const UP_KEY_CODE = 38; ...@@ -6,3 +6,7 @@ export const UP_KEY_CODE = 38;
export const DOWN_KEY_CODE = 40; export const DOWN_KEY_CODE = 40;
export const ENTER_KEY_CODE = 13; export const ENTER_KEY_CODE = 13;
export const ESC_KEY_CODE = 27; export const ESC_KEY_CODE = 27;
// Commit message textarea
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) => { ) =>
new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) { if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, { entry: state }); commit(types.TOGGLE_LOADING, { entry: state });
service.getProjectData(namespace, projectId) service
.getProjectData(namespace, projectId)
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
commit(types.TOGGLE_LOADING, { entry: state }); commit(types.TOGGLE_LOADING, { entry: state });
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); if (!state.currentProjectId)
commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data); resolve(data);
}) })
.catch(() => { .catch(() => {
flash('Error loading project data. Please try again.', 'alert', document, null, false, true); flash(
'Error loading project data. Please try again.',
'alert',
document,
null,
false,
true,
);
reject(new Error(`Project not loaded ${namespace}/${projectId}`)); reject(new Error(`Project not loaded ${namespace}/${projectId}`));
}); });
} else { } else {
resolve(state.projects[`${namespace}/${projectId}`]); 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] ||
force
) {
service
.getBranchData(`${projectId}`, branchId)
.then(({ data }) => { .then(({ data }) => {
const { id } = data.commit; const { id } = data.commit;
commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data }); commit(types.SET_BRANCH, {
projectPath: `${projectId}`,
branchName: branchId,
branch: data,
});
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
commit(types.SET_CURRENT_BRANCH, branchId);
resolve(data); resolve(data);
}) })
.catch(() => { .catch(() => {
flash('Error loading branch data. Please try again.', 'alert', document, null, false, true); flash(
'Error loading branch data. Please try again.',
'alert',
document,
null,
false,
true,
);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
}); });
} else { } else {
resolve(state.projects[`${projectId}`].branches[branchId]); 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, {
[treePath]: {
tree: data, tree: data,
},
}),
}); });
}, },
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) { [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
......
import $ from 'jquery'; import $ from 'jquery';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import flash from './flash'; import flash from './flash';
import { mouseenter, debouncedMouseleave, togglePopover } from './shared/popover';
export default class Milestone { export default class Milestone {
constructor() { constructor() {
...@@ -43,4 +44,25 @@ export default class Milestone { ...@@ -43,4 +44,25 @@ export default class Milestone {
.catch(() => flash('Error loading milestone tab')); .catch(() => flash('Error loading milestone tab'));
} }
} }
static initDeprecationMessage() {
const deprecationMesssageContainer = document.querySelector('.js-milestone-deprecation-message');
if (!deprecationMesssageContainer) return;
const deprecationMessage = deprecationMesssageContainer.querySelector('.js-milestone-deprecation-message-template').innerHTML;
const $popover = $('.js-popover-link', deprecationMesssageContainer);
const hideOnScroll = togglePopover.bind($popover, false);
$popover.popover({
content: deprecationMessage,
html: true,
placement: 'bottom',
})
.on('mouseenter', mouseenter)
.on('mouseleave', debouncedMouseleave())
.on('show.bs.popover', () => {
window.addEventListener('scroll', hideOnScroll, { once: true });
});
}
} }
...@@ -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,21 +1450,36 @@ export default class Notes { ...@@ -1449,21 +1450,36 @@ 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) {
const $element = $(e.currentTarget); const $element = $(e.currentTarget);
......
...@@ -6,4 +6,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -6,4 +6,6 @@ document.addEventListener('DOMContentLoaded', () => {
new Milestone(); // eslint-disable-line no-new new Milestone(); // eslint-disable-line no-new
new Sidebar(); // eslint-disable-line no-new new Sidebar(); // eslint-disable-line no-new
new MountMilestoneSidebar(); // eslint-disable-line no-new new MountMilestoneSidebar(); // eslint-disable-line no-new
Milestone.initDeprecationMessage();
}); });
import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
import Milestone from '~/milestone';
document.addEventListener('DOMContentLoaded', initMilestonesShow); document.addEventListener('DOMContentLoaded', () => {
initMilestonesShow();
Milestone.initDeprecationMessage();
});
import initNotes from '~/init_notes'; import initNotes from '~/init_notes';
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
import LineHighlighter from '../../../../line_highlighter'; import LineHighlighter from '~/line_highlighter';
import BlobViewer from '../../../../blob/viewer'; import BlobViewer from '~/blob/viewer';
import snippetEmbed from '~/snippet/snippet_embed';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new LineHighlighter(); // eslint-disable-line no-new new LineHighlighter(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new
initNotes(); initNotes();
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
snippetEmbed();
}); });
import LineHighlighter from '../../../line_highlighter'; import LineHighlighter from '~/line_highlighter';
import BlobViewer from '../../../blob/viewer'; import BlobViewer from '~/blob/viewer';
import ZenMode from '../../../zen_mode'; import ZenMode from '~/zen_mode';
import initNotes from '../../../init_notes'; import initNotes from '~/init_notes';
import snippetEmbed from '~/snippet/snippet_embed';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new LineHighlighter(); // eslint-disable-line no-new new LineHighlighter(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new
initNotes(); initNotes();
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
snippetEmbed();
}); });
...@@ -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 requestId = response.headers['x-request-id'];
const requestUrl = response.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);
} }
});
};
Vue.http.interceptors.push(vueResourceInterceptor); return response;
};
return axios.interceptors.response.use(response => { vueResourceInterceptor = (request, next) => next(interceptor);
const requestId = response.headers['x-request-id'];
const requestUrl = response.config.url;
if (requestUrl !== peekUrl && requestId) { Vue.http.interceptors.push(vueResourceInterceptor);
callback(requestId, requestUrl);
}
return response; return axios.interceptors.response.use(interceptor);
});
} }
static removeInterceptor(interceptor) { static removeInterceptor(interceptor) {
......
<script> <script>
import $ from 'jquery';
/** /**
* Renders each stage of the pipeline mini graph. * Renders each stage of the pipeline mini graph.
...@@ -13,8 +12,11 @@ ...@@ -13,8 +12,11 @@
* 3. Merge request widget * 3. Merge request widget
* 4. Commit widget * 4. Commit widget
*/ */
import axios from '../../lib/utils/axios_utils';
import $ from 'jquery';
import Flash from '../../flash'; import Flash from '../../flash';
import axios from '../../lib/utils/axios_utils';
import eventHub from '../event_hub';
import Icon from '../../vue_shared/components/icon.vue'; import Icon from '../../vue_shared/components/icon.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
...@@ -82,6 +84,7 @@ ...@@ -82,6 +84,7 @@
methods: { methods: {
onClickStage() { onClickStage() {
if (!this.isDropdownOpen()) { if (!this.isDropdownOpen()) {
eventHub.$emit('clickedDropdown');
this.isLoading = true; this.isLoading = true;
this.fetchJobs(); this.fetchJobs();
} }
......
// eslint-disable-next-line import/prefer-default-export
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
...@@ -7,6 +7,7 @@ import SvgBlankState from '../components/blank_state.vue'; ...@@ -7,6 +7,7 @@ import SvgBlankState from '../components/blank_state.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import PipelinesTableComponent from '../components/pipelines_table.vue'; import PipelinesTableComponent from '../components/pipelines_table.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { CANCEL_REQUEST } from '../constants';
export default { export default {
components: { components: {
...@@ -52,34 +53,58 @@ export default { ...@@ -52,34 +53,58 @@ export default {
}); });
eventHub.$on('postAction', this.postAction); eventHub.$on('postAction', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('postAction', this.postAction); eventHub.$off('postAction', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
}, },
destroyed() { destroyed() {
this.poll.stop(); this.poll.stop();
}, },
methods: { methods: {
updateTable() {
// Cancel ongoing request
if (this.isMakingRequest) {
this.service.cancelationSource.cancel(CANCEL_REQUEST);
}
// Stop polling
this.poll.stop();
// Update the table
return this.getPipelines()
.then(() => this.poll.restart());
},
fetchPipelines() { fetchPipelines() {
if (!this.isMakingRequest) { if (!this.isMakingRequest) {
this.isLoading = true; this.isLoading = true;
this.service.getPipelines(this.requestData) this.getPipelines();
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
} }
}, },
getPipelines() {
return this.service.getPipelines(this.requestData)
.then(response => this.successCallback(response))
.catch((error) => this.errorCallback(error));
},
setCommonData(pipelines) { setCommonData(pipelines) {
this.store.storePipelines(pipelines); this.store.storePipelines(pipelines);
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = true; this.updateGraphDropdown = true;
this.hasMadeRequest = true; this.hasMadeRequest = true;
// In case the previous polling request returned an error, we need to reset it
if (this.hasError) {
this.hasError = false;
}
}, },
errorCallback() { errorCallback(error) {
this.hasError = true; this.hasMadeRequest = true;
this.isLoading = false; this.isLoading = false;
if (error && error.message && error.message !== CANCEL_REQUEST) {
this.hasError = true;
this.updateGraphDropdown = false; this.updateGraphDropdown = false;
this.hasMadeRequest = true; }
}, },
setIsMakingRequest(isMakingRequest) { setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest; this.isMakingRequest = isMakingRequest;
......
...@@ -19,8 +19,13 @@ export default class PipelinesService { ...@@ -19,8 +19,13 @@ export default class PipelinesService {
getPipelines(data = {}) { getPipelines(data = {}) {
const { scope, page } = data; const { scope, page } = data;
const CancelToken = axios.CancelToken;
this.cancelationSource = CancelToken.source();
return axios.get(this.endpoint, { return axios.get(this.endpoint, {
params: { scope, page }, params: { scope, page },
cancelToken: this.cancelationSource.token,
}); });
} }
......
import $ from 'jquery';
import _ from 'underscore';
export function togglePopover(show) {
const isAlreadyShown = this.hasClass('js-popover-show');
if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) {
return false;
}
this.popover(show ? 'show' : 'hide');
this.toggleClass('disable-animation js-popover-show', show);
return true;
}
export function mouseleave() {
if (!$('.popover:hover').length > 0) {
const $popover = $(this);
togglePopover.call($popover, false);
}
}
export function mouseenter() {
const $popover = $(this);
const showedPopover = togglePopover.call($popover, true);
if (showedPopover) {
$('.popover').on('mouseleave', mouseleave.bind($popover));
}
}
export function debouncedMouseleave(debounceTimeout = 300) {
return _.debounce(mouseleave, debounceTimeout);
}
import { visitUrl } from './lib/utils/url_utility';
/** /**
* Helper function that finds the href of the fiven selector and updates the location. * Helper function that finds the href of the fiven selector and updates the location.
* *
* @param {String} selector * @param {String} selector
*/ */
export default (selector) => { export default function findAndFollowLink(selector) {
const link = document.querySelector(selector).getAttribute('href'); const element = document.querySelector(selector);
const link = element && element.getAttribute('href');
if (link) { if (link) {
window.location = link; visitUrl(link);
} }
}; }
export default () => {
const { protocol, host, pathname } = location;
const shareBtn = document.querySelector('.js-share-btn');
const embedBtn = document.querySelector('.js-embed-btn');
const snippetUrlArea = document.querySelector('.js-snippet-url-area');
const embedAction = document.querySelector('.js-embed-action');
const url = `${protocol}//${host + pathname}`;
shareBtn.addEventListener('click', () => {
shareBtn.classList.add('is-active');
embedBtn.classList.remove('is-active');
snippetUrlArea.value = url;
embedAction.innerText = 'Share';
});
embedBtn.addEventListener('click', () => {
embedBtn.classList.add('is-active');
shareBtn.classList.remove('is-active');
const scriptTag = `<script src="${url}.js"></script>`;
snippetUrlArea.value = scriptTag;
embedAction.innerText = 'Embed';
});
};
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>
<script>
import successSvg from 'icons/_icon_status_success.svg'; import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg'; import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll'; import simplePoll from '~/lib/utils/simple_poll';
...@@ -7,7 +8,10 @@ import statusIcon from '../mr_widget_status_icon.vue'; ...@@ -7,7 +8,10 @@ import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
name: 'MRWidgetReadyToMerge', name: 'ReadyToMerge',
components: {
statusIcon,
},
props: { props: {
mr: { type: Object, required: true }, mr: { type: Object, required: true },
service: { type: Object, required: true }, service: { type: Object, required: true },
...@@ -26,9 +30,6 @@ export default { ...@@ -26,9 +30,6 @@ export default {
warningSvg, warningSvg,
}; };
}, },
components: {
statusIcon,
},
computed: { computed: {
shouldShowMergeWhenPipelineSucceedsText() { shouldShowMergeWhenPipelineSucceedsText() {
return this.mr.isPipelineActive; return this.mr.isPipelineActive;
...@@ -217,7 +218,10 @@ export default { ...@@ -217,7 +218,10 @@ export default {
}); });
}, },
}, },
template: ` };
</script>
<template>
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon :status="iconClass" /> <status-icon :status="iconClass" />
<div class="media-body"> <div class="media-body">
...@@ -232,8 +236,9 @@ export default { ...@@ -232,8 +236,9 @@ export default {
<i <i
v-if="isMakingRequest" v-if="isMakingRequest"
class="fa fa-spinner fa-spin" class="fa fa-spinner fa-spin"
aria-hidden="true" /> aria-hidden="true"
{{mergeButtonText}} ></i>
{{ mergeButtonText }}
</button> </button>
<button <button
v-if="shouldShowMergeOptionsDropdown" v-if="shouldShowMergeOptionsDropdown"
...@@ -244,7 +249,8 @@ export default { ...@@ -244,7 +249,8 @@ export default {
aria-label="Select merge moment"> aria-label="Select merge moment">
<i <i
class="fa fa-chevron-down" class="fa fa-chevron-down"
aria-hidden="true" /> aria-hidden="true"
></i>
</button> </button>
<ul <ul
v-if="shouldShowMergeOptionsDropdown" v-if="shouldShowMergeOptionsDropdown"
...@@ -331,22 +337,27 @@ export default { ...@@ -331,22 +337,27 @@ export default {
<div class="commit-message-container"> <div class="commit-message-container">
<div class="max-width-marker"></div> <div class="max-width-marker"></div>
<textarea <textarea
id="commit-message"
v-model="commitMessage" v-model="commitMessage"
class="form-control js-commit-message" class="form-control js-commit-message"
required="required" required="required"
rows="14" rows="14"
name="Commit message"></textarea> name="Commit message"></textarea>
</div> </div>
<p class="hint">Try to keep the first line under 52 characters and the others under 72</p> <p class="hint">
Try to keep the first line under 52 characters and the others under 72
</p>
<div class="hint"> <div class="hint">
<a <a
@click.prevent="updateCommitMessage" @click.prevent="updateCommitMessage"
href="#">{{commitMessageLinkTitle}}</a> href="#"
>
{{ commitMessageLinkTitle }}
</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
`, </template>
};
...@@ -27,11 +27,11 @@ export { default as ConflictsState } from './components/states/mr_widget_conflic ...@@ -27,11 +27,11 @@ export { default as ConflictsState } from './components/states/mr_widget_conflic
export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue'; export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue'; export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue'; export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue';
export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge'; export { default as ReadyToMergeState } from './components/states/ready_to_merge.vue';
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>
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
lines: { lines: {
type: Number, type: Number,
required: false, required: false,
default: 6, default: 3,
}, },
}, },
computed: { computed: {
......
...@@ -37,7 +37,11 @@ ...@@ -37,7 +37,11 @@
/* /*
* Code highlight * Code highlight
*/ */
@import "highlight/**/*"; @import "highlight/dark";
@import "highlight/monokai";
@import "highlight/solarized_dark";
@import "highlight/solarized_light";
@import "highlight/white";
/* /*
* Styles for JS behaviors. * Styles for JS behaviors.
......
...@@ -187,12 +187,9 @@ a { ...@@ -187,12 +187,9 @@ a {
animation: fadeInFull $fade-in-duration 1; animation: fadeInFull $fade-in-duration 1;
} }
.animation-container { .animation-container {
background: $repo-editor-grey;
height: 40px; height: 40px;
overflow: hidden; overflow: hidden;
position: relative;
&.animation-container-small { &.animation-container-small {
height: 12px; height: 12px;
...@@ -205,60 +202,43 @@ a { ...@@ -205,60 +202,43 @@ a {
} }
} }
&::before { [class^="skeleton-line-"] {
animation-duration: 1s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: blockTextShine;
animation-timing-function: linear;
background-image: $repo-editor-linear-gradient;
background-repeat: no-repeat;
background-size: 800px 45px;
content: ' ';
display: block;
height: 100%;
position: relative; position: relative;
} background-color: $theme-gray-100;
height: 10px;
div { overflow: hidden;
background: $white-light;
height: 6px;
left: 0;
position: absolute;
right: 0;
}
.skeleton-line-1 { &:not(:last-of-type) {
left: 0; margin-bottom: 4px;
top: 8px;
} }
.skeleton-line-2 { &::after {
left: 150px; content: ' ';
top: 0; display: block;
animation: blockTextShine 1s linear infinite forwards;
background-repeat: no-repeat;
background-size: cover;
background-image: linear-gradient(
to right,
$theme-gray-100 0%,
$theme-gray-50 20%,
$theme-gray-100 40%,
$theme-gray-100 100%
);
height: 10px; height: 10px;
} }
.skeleton-line-3 {
left: 0;
top: 23px;
}
.skeleton-line-4 {
left: 0;
top: 38px;
} }
}
.skeleton-line-5 { $skeleton-line-widths: (
left: 200px; 156px,
top: 28px; 235px,
height: 10px; 200px,
} );
.skeleton-line-6 { @for $count from 1 through length($skeleton-line-widths) {
top: 14px; .skeleton-line-#{$count} {
left: 230px; width: nth($skeleton-line-widths, $count);
height: 10px;
} }
} }
......
.banner-callout { .banner-callout {
display: flex; display: flex;
position: relative; position: relative;
flex-wrap: wrap; align-items: start;
.banner-close { .banner-close {
position: absolute; position: absolute;
...@@ -16,10 +16,25 @@ ...@@ -16,10 +16,25 @@
} }
.banner-graphic { .banner-graphic {
margin: 20px auto; margin: 0 $gl-padding $gl-padding 0;
} }
&.banner-non-empty-state { &.banner-non-empty-state {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
@media (max-width: $screen-xs-max) {
justify-content: center;
flex-direction: column;
align-items: center;
.banner-title,
.banner-buttons {
text-align: center;
}
.banner-graphic {
margin-left: $gl-padding;
}
}
} }
...@@ -422,7 +422,24 @@ ...@@ -422,7 +422,24 @@
} }
} }
.btn-link.btn-secondary-hover-link { .btn-link {
padding: 0;
background-color: transparent;
color: $blue-600;
font-weight: normal;
border-radius: 0;
border-color: transparent;
&:hover,
&:active,
&:focus {
color: $blue-800;
text-decoration: underline;
background-color: transparent;
border-color: transparent;
}
&.btn-secondary-hover-link {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
&:hover, &:hover,
...@@ -431,9 +448,9 @@ ...@@ -431,9 +448,9 @@
color: $gl-link-color; color: $gl-link-color;
text-decoration: none; text-decoration: none;
} }
} }
.btn-link.btn-primary-hover-link { &.btn-primary-hover-link {
color: inherit; color: inherit;
&:hover, &:hover,
...@@ -442,6 +459,7 @@ ...@@ -442,6 +459,7 @@
color: $gl-link-color; color: $gl-link-color;
text-decoration: none; text-decoration: none;
} }
}
} }
.btn-missing { .btn-missing {
...@@ -485,3 +503,7 @@ fieldset[disabled] .btn, ...@@ -485,3 +503,7 @@ fieldset[disabled] .btn,
@extend %disabled; @extend %disabled;
} }
} }
.btn-no-padding {
padding: 0;
}
...@@ -485,7 +485,8 @@ ...@@ -485,7 +485,8 @@
.dropdown-menu-selectable { .dropdown-menu-selectable {
li { li {
a { a,
button {
padding: 8px 40px; padding: 8px 40px;
position: relative; position: relative;
......
...@@ -29,8 +29,10 @@ ...@@ -29,8 +29,10 @@
} }
.snippet-title { .snippet-title {
font-size: 24px; color: $gl-text-color;
font-size: 2em;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
min-height: $header-height;
} }
.snippet-edited-ago { .snippet-edited-ago {
...@@ -46,3 +48,26 @@ ...@@ -46,3 +48,26 @@
.snippet-scope-menu .btn-new { .snippet-scope-menu .btn-new {
margin-top: 15px; margin-top: 15px;
} }
.snippet-embed-input {
height: 35px;
}
.embed-snippet {
padding-right: 0;
padding-top: $gl-padding;
.form-control {
cursor: auto;
width: 101%;
margin-left: -1px;
}
.embed-toggle-list li button {
padding: 8px 40px;
}
.embed-toggle {
height: 35px;
}
}
...@@ -713,20 +713,6 @@ $color-high-score: $green-400; ...@@ -713,20 +713,6 @@ $color-high-score: $green-400;
$color-average-score: $orange-400; $color-average-score: $orange-400;
$color-low-score: $red-400; $color-low-score: $red-400;
/*
Repo editor
*/
$repo-editor-grey: #f6f7f9;
$repo-editor-grey-darker: #e9ebee;
$repo-editor-linear-gradient: linear-gradient(
to right,
$repo-editor-grey 0%,
$repo-editor-grey-darker,
20%,
$repo-editor-grey 40%,
$repo-editor-grey 100%
);
/* /*
Performance Bar Performance Bar
*/ */
......
/* https://github.com/aahan/pygments-github-style */
/*
* White Syntax Colors
*/
$white-code-color: $gl-text-color;
$white-highlight: #fafe3d;
$white-pre-hll-bg: #f8eec7;
$white-hll-bg: #f8f8f8;
$white-over-bg: #ded7fc;
$white-expanded-border: #e0e0e0;
$white-expanded-bg: #f7f7f7;
$white-c: #998;
$white-err: #a61717;
$white-err-bg: #e3d2d2;
$white-cm: #998;
$white-cp: #999;
$white-c1: #998;
$white-cs: #999;
$white-gd: $black;
$white-gd-bg: #fdd;
$white-gd-x: $black;
$white-gd-x-bg: #faa;
$white-gr: #a00;
$white-gh: #999;
$white-gi: $black;
$white-gi-bg: #dfd;
$white-gi-x: $black;
$white-gi-x-bg: #afa;
$white-go: #888;
$white-gp: #555;
$white-gu: #800080;
$white-gt: #a00;
$white-kt: #458;
$white-m: #099;
$white-s: #d14;
$white-n: #333;
$white-na: teal;
$white-nb: #0086b3;
$white-nc: #458;
$white-no: teal;
$white-ni: purple;
$white-ne: #900;
$white-nf: #900;
$white-nn: #555;
$white-nt: navy;
$white-nv: teal;
$white-w: #bbb;
$white-mf: #099;
$white-mh: #099;
$white-mi: #099;
$white-mo: #099;
$white-sb: #d14;
$white-sc: #d14;
$white-sd: #d14;
$white-s2: #d14;
$white-se: #d14;
$white-sh: #d14;
$white-si: #d14;
$white-sx: #d14;
$white-sr: #009926;
$white-s1: #d14;
$white-ss: #990073;
$white-bp: #999;
$white-vc: teal;
$white-vg: teal;
$white-vi: teal;
$white-il: #099;
$white-gc-color: #999;
$white-gc-bg: #eaf2f5;
@mixin matchLine {
color: $black-transparent;
background-color: $gray-light;
}
.code.white { .code.white {
// Line numbers @import "white_base";
.line-numbers,
.diff-line-num {
background-color: $gray-light;
}
.diff-line-num,
.diff-line-num a {
color: $black-transparent;
}
// Code itself
pre.code,
.diff-line-num {
border-color: $white-normal;
}
&,
pre.code,
.line_holder .line_content {
background-color: $white-light;
color: $white-code-color;
}
// Diff line
.line_holder {
&.match .line_content {
@include matchLine;
}
.diff-line-num {
&.old {
background-color: $line-number-old;
border-color: $line-removed-dark;
a {
color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
}
}
&.new {
background-color: $line-number-new;
border-color: $line-added-dark;
a {
color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
}
}
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $white-over-bg;
border-color: darken($white-over-bg, 5%);
a {
color: darken($white-over-bg, 15%);
}
}
&.hll:not(.empty-cell) {
background-color: $line-number-select;
border-color: $line-select-yellow-dark;
}
}
&:not(.diff-expanded) + .diff-expanded,
&.diff-expanded + .line_holder:not(.diff-expanded) {
> .diff-line-num,
> .line_content {
border-top: 1px solid $white-expanded-border;
}
}
&.diff-expanded {
> .diff-line-num,
> .line_content {
background: $white-expanded-bg;
border-color: $white-expanded-bg;
}
}
.line_content {
&.old {
background-color: $line-removed;
&::before {
color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
}
span.idiff {
background-color: $line-removed-dark;
}
}
&.new {
background-color: $line-added;
&::before {
color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
}
span.idiff {
background-color: $line-added-dark;
}
}
&.match {
@include matchLine;
}
&.hll:not(.empty-cell) {
background-color: $line-select-yellow;
}
}
}
// highlight line via anchor
pre .hll {
background-color: $white-pre-hll-bg !important;
}
// Search result highlight
span.highlight_word {
background-color: $white-highlight !important;
}
// Links to URLs, emails, or dependencies
.line a {
color: $white-nb;
}
.hll { background-color: $white-hll-bg; }
.c { color: $white-c; font-style: italic; }
.err { color: $white-err; background-color: $white-err-bg; }
.k { font-weight: $gl-font-weight-bold; }
.o { font-weight: $gl-font-weight-bold; }
.cm { color: $white-cm; font-style: italic; }
.cp { color: $white-cp; font-weight: $gl-font-weight-bold; }
.c1 { color: $white-c1; font-style: italic; }
.cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
.gd {
color: $white-gd;
background-color: $white-gd-bg;
.x {
color: $white-gd-x;
background-color: $white-gd-x-bg;
}
}
.ge { font-style: italic; }
.gr { color: $white-gr; }
.gh { color: $white-gh; }
.gi {
color: $white-gi;
background-color: $white-gi-bg;
.x {
color: $white-gi-x;
background-color: $white-gi-x-bg;
}
}
.go { color: $white-go; }
.gp { color: $white-gp; }
.gs { font-weight: $gl-font-weight-bold; }
.gu { color: $white-gu; font-weight: $gl-font-weight-bold; }
.gt { color: $white-gt; }
.kc { font-weight: $gl-font-weight-bold; }
.kd { font-weight: $gl-font-weight-bold; }
.kn { font-weight: $gl-font-weight-bold; }
.kp { font-weight: $gl-font-weight-bold; }
.kr { font-weight: $gl-font-weight-bold; }
.kt { color: $white-kt; font-weight: $gl-font-weight-bold; }
.m { color: $white-m; }
.s { color: $white-s; }
.n { color: $white-n; }
.na { color: $white-na; }
.nb { color: $white-nb; }
.nc { color: $white-nc; font-weight: $gl-font-weight-bold; }
.no { color: $white-no; }
.ni { color: $white-ni; }
.ne { color: $white-ne; font-weight: $gl-font-weight-bold; }
.nf { color: $white-nf; font-weight: $gl-font-weight-bold; }
.nn { color: $white-nn; }
.nt { color: $white-nt; }
.nv { color: $white-nv; }
.ow { font-weight: $gl-font-weight-bold; }
.w { color: $white-w; }
.mf { color: $white-mf; }
.mh { color: $white-mh; }
.mi { color: $white-mi; }
.mo { color: $white-mo; }
.sb { color: $white-sb; }
.sc { color: $white-sc; }
.sd { color: $white-sd; }
.s2 { color: $white-s2; }
.se { color: $white-se; }
.sh { color: $white-sh; }
.si { color: $white-si; }
.sx { color: $white-sx; }
.sr { color: $white-sr; }
.s1 { color: $white-s1; }
.ss { color: $white-ss; }
.bp { color: $white-bp; }
.vc { color: $white-vc; }
.vg { color: $white-vg; }
.vi { color: $white-vi; }
.il { color: $white-il; }
.gc { color: $white-gc-color; background-color: $white-gc-bg; }
} }
/* https://github.com/aahan/pygments-github-style */
/*
* White Syntax Colors
*/
$white-code-color: $gl-text-color;
$white-highlight: #fafe3d;
$white-pre-hll-bg: #f8eec7;
$white-hll-bg: #f8f8f8;
$white-over-bg: #ded7fc;
$white-expanded-border: #e0e0e0;
$white-expanded-bg: #f7f7f7;
$white-c: #998;
$white-err: #a61717;
$white-err-bg: #e3d2d2;
$white-cm: #998;
$white-cp: #999;
$white-c1: #998;
$white-cs: #999;
$white-gd: $black;
$white-gd-bg: #fdd;
$white-gd-x: $black;
$white-gd-x-bg: #faa;
$white-gr: #a00;
$white-gh: #999;
$white-gi: $black;
$white-gi-bg: #dfd;
$white-gi-x: $black;
$white-gi-x-bg: #afa;
$white-go: #888;
$white-gp: #555;
$white-gu: #800080;
$white-gt: #a00;
$white-kt: #458;
$white-m: #099;
$white-s: #d14;
$white-n: #333;
$white-na: teal;
$white-nb: #0086b3;
$white-nc: #458;
$white-no: teal;
$white-ni: purple;
$white-ne: #900;
$white-nf: #900;
$white-nn: #555;
$white-nt: navy;
$white-nv: teal;
$white-w: #bbb;
$white-mf: #099;
$white-mh: #099;
$white-mi: #099;
$white-mo: #099;
$white-sb: #d14;
$white-sc: #d14;
$white-sd: #d14;
$white-s2: #d14;
$white-se: #d14;
$white-sh: #d14;
$white-si: #d14;
$white-sx: #d14;
$white-sr: #009926;
$white-s1: #d14;
$white-ss: #990073;
$white-bp: #999;
$white-vc: teal;
$white-vg: teal;
$white-vi: teal;
$white-il: #099;
$white-gc-color: #999;
$white-gc-bg: #eaf2f5;
@mixin matchLine {
color: $black-transparent;
background-color: $gray-light;
}
// Line numbers
.line-numbers,
.diff-line-num {
background-color: $gray-light;
}
.diff-line-num,
.diff-line-num a {
color: $black-transparent;
}
// Code itself
pre.code,
.diff-line-num {
border-color: $white-normal;
}
&,
pre.code,
.line_holder .line_content {
background-color: $white-light;
color: $white-code-color;
}
// Diff line
.line_holder {
&.match .line_content {
@include matchLine;
}
.diff-line-num {
&.old {
background-color: $line-number-old;
border-color: $line-removed-dark;
a {
color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
}
}
&.new {
background-color: $line-number-new;
border-color: $line-added-dark;
a {
color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
}
}
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $white-over-bg;
border-color: darken($white-over-bg, 5%);
a {
color: darken($white-over-bg, 15%);
}
}
&.hll:not(.empty-cell) {
background-color: $line-number-select;
border-color: $line-select-yellow-dark;
}
}
&:not(.diff-expanded) + .diff-expanded,
&.diff-expanded + .line_holder:not(.diff-expanded) {
> .diff-line-num,
> .line_content {
border-top: 1px solid $white-expanded-border;
}
}
&.diff-expanded {
> .diff-line-num,
> .line_content {
background: $white-expanded-bg;
border-color: $white-expanded-bg;
}
}
.line_content {
&.old {
background-color: $line-removed;
&::before {
color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
}
span.idiff {
background-color: $line-removed-dark;
}
}
&.new {
background-color: $line-added;
&::before {
color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
}
span.idiff {
background-color: $line-added-dark;
}
}
&.match {
@include matchLine;
}
&.hll:not(.empty-cell) {
background-color: $line-select-yellow;
}
}
}
// highlight line via anchor
pre .hll {
background-color: $white-pre-hll-bg !important;
}
// Search result highlight
span.highlight_word {
background-color: $white-highlight !important;
}
// Links to URLs, emails, or dependencies
.line a {
color: $white-nb;
}
.hll { background-color: $white-hll-bg; }
.c { color: $white-c; font-style: italic; }
.err { color: $white-err; background-color: $white-err-bg; }
.k { font-weight: $gl-font-weight-bold; }
.o { font-weight: $gl-font-weight-bold; }
.cm { color: $white-cm; font-style: italic; }
.cp { color: $white-cp; font-weight: $gl-font-weight-bold; }
.c1 { color: $white-c1; font-style: italic; }
.cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
.gd {
color: $white-gd;
background-color: $white-gd-bg;
.x {
color: $white-gd-x;
background-color: $white-gd-x-bg;
}
}
.ge { font-style: italic; }
.gr { color: $white-gr; }
.gh { color: $white-gh; }
.gi {
color: $white-gi;
background-color: $white-gi-bg;
.x {
color: $white-gi-x;
background-color: $white-gi-x-bg;
}
}
.go { color: $white-go; }
.gp { color: $white-gp; }
.gs { font-weight: $gl-font-weight-bold; }
.gu { color: $white-gu; font-weight: $gl-font-weight-bold; }
.gt { color: $white-gt; }
.kc { font-weight: $gl-font-weight-bold; }
.kd { font-weight: $gl-font-weight-bold; }
.kn { font-weight: $gl-font-weight-bold; }
.kp { font-weight: $gl-font-weight-bold; }
.kr { font-weight: $gl-font-weight-bold; }
.kt { color: $white-kt; font-weight: $gl-font-weight-bold; }
.m { color: $white-m; }
.s { color: $white-s; }
.n { color: $white-n; }
.na { color: $white-na; }
.nb { color: $white-nb; }
.nc { color: $white-nc; font-weight: $gl-font-weight-bold; }
.no { color: $white-no; }
.ni { color: $white-ni; }
.ne { color: $white-ne; font-weight: $gl-font-weight-bold; }
.nf { color: $white-nf; font-weight: $gl-font-weight-bold; }
.nn { color: $white-nn; }
.nt { color: $white-nt; }
.nv { color: $white-nv; }
.ow { font-weight: $gl-font-weight-bold; }
.w { color: $white-w; }
.mf { color: $white-mf; }
.mh { color: $white-mh; }
.mi { color: $white-mi; }
.mo { color: $white-mo; }
.sb { color: $white-sb; }
.sc { color: $white-sc; }
.sd { color: $white-sd; }
.s2 { color: $white-s2; }
.se { color: $white-se; }
.sh { color: $white-sh; }
.si { color: $white-si; }
.sx { color: $white-sx; }
.sr { color: $white-sr; }
.s1 { color: $white-s1; }
.ss { color: $white-ss; }
.bp { color: $white-bp; }
.vc { color: $white-vc; }
.vg { color: $white-vg; }
.vi { color: $white-vi; }
.il { color: $white-il; }
.gc { color: $white-gc-color; background-color: $white-gc-bg; }
...@@ -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;
}
}
}
...@@ -194,3 +194,38 @@ ...@@ -194,3 +194,38 @@
.issuable-row { .issuable-row {
background-color: $white-light; background-color: $white-light;
} }
.milestone-deprecation-message {
.popover {
padding: 0;
}
.popover-content {
padding: 0;
}
}
.milestone-popover-body {
padding: $gl-padding-8;
background-color: $gray-light;
}
.milestone-popover-footer {
padding: $gl-padding-8 $gl-padding;
border-top: 1px solid $white-dark;
}
.milestone-popover-instructions-list {
padding-left: 2em;
> li {
padding-left: 1em;
}
}
@media (max-width: $screen-xs-max) {
.milestone-banner-text,
.milestone-banner-link {
display: inline;
}
}
...@@ -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 {
......
...@@ -663,11 +663,6 @@ ...@@ -663,11 +663,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;
...@@ -860,3 +855,74 @@ ...@@ -860,3 +855,74 @@
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;
}
@import "framework/variables";
.gitlab-embed-snippets {
@import "highlight/embedded";
@import "framework/images";
$border-style: 1px solid $border-color;
font-family: $regular_font;
font-size: $gl-font-size;
line-height: $code_line_height;
color: $gl-text-color;
margin: 20px;
font-weight: 200;
.gl-snippet-icon {
display: inline-block;
background: url(asset_path('ext_snippet_icons/ext_snippet_icons.png')) no-repeat;
overflow: hidden;
text-align: left;
width: 16px;
height: 16px;
background-size: cover;
&.gl-snippet-icon-doc_code { background-position: 0 0; }
&.gl-snippet-icon-doc_text { background-position: 0 -16px; }
&.gl-snippet-icon-download { background-position: 0 -32px; }
}
.blob-viewer {
background-color: $white-light;
text-align: left;
}
.file-content.code {
border: $border-style;
border-radius: 0 0 4px 4px;
display: flex;
box-shadow: none;
margin: 0;
padding: 0;
table-layout: fixed;
.blob-content {
overflow-x: auto;
pre {
padding: 10px;
border: 0;
border-radius: 0;
font-family: $monospace_font;
font-size: $code_font_size;
line-height: $code_line_height;
margin: 0;
overflow: auto;
overflow-y: hidden;
white-space: pre;
word-wrap: normal;
border-left: $border-style;
}
}
.line-numbers {
padding: 10px;
text-align: right;
float: left;
.diff-line-num {
font-family: $monospace_font;
display: block;
font-size: $code_font_size;
min-height: $code_line_height;
white-space: nowrap;
color: $black-transparent;
min-width: 30px;
}
.diff-line-num:hover {
color: $almost-black;
cursor: pointer;
}
}
}
.file-title-flex-parent {
display: flex;
align-items: center;
justify-content: space-between;
background-color: $gray-light;
border: $border-style;
border-bottom: 0;
padding: $gl-padding-top $gl-padding;
margin: 0;
border-radius: $border-radius-default $border-radius-default 0 0;
.file-header-content {
.file-title-name {
font-weight: $gl-font-weight-bold;
}
.gitlab-embedded-snippets-title {
text-decoration: none;
color: $gl-text-color;
&:hover {
text-decoration: underline;
}
}
.gitlab-logo {
display: inline-block;
padding-left: 5px;
text-decoration: none;
color: $gl-text-color-secondary;
.logo-text {
background: image_url('ext_snippet_icons/logo.png') no-repeat left center;
background-size: 18px;
font-weight: $gl-font-weight-normal;
padding-left: 24px;
}
}
}
img,
.gl-snippet-icon {
display: inline-block;
vertical-align: middle;
}
}
.btn-group {
a.btn {
background-color: $white-light;
text-decoration: none;
padding: 7px 9px;
border: $border-style;
border-right: 0;
&:hover {
background-color: $white-normal;
border-color: $border-white-normal;
text-decoration: none;
}
&:first-child {
border-radius: 3px 0 0 3px;
}
&:last-child {
border-radius: 0 3px 3px 0;
border-right: $border-style;
}
}
}
}
...@@ -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
......
...@@ -17,6 +17,10 @@ module SnippetsActions ...@@ -17,6 +17,10 @@ module SnippetsActions
end end
# rubocop:enable Gitlab/ModuleWithInstanceVariables # rubocop:enable Gitlab/ModuleWithInstanceVariables
def js_request?
request.format.js?
end
private private
def convert_line_endings(content) def convert_line_endings(content)
......
...@@ -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
......
...@@ -5,6 +5,8 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -5,6 +5,8 @@ class Projects::SnippetsController < Projects::ApplicationController
include SnippetsActions include SnippetsActions
include RendersBlob include RendersBlob
skip_before_action :verify_authenticity_token, only: [:show], if: :js_request?
before_action :check_snippets_available! before_action :check_snippets_available!
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
...@@ -71,6 +73,7 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -71,6 +73,7 @@ class Projects::SnippetsController < Projects::ApplicationController
format.json do format.json do
render_blob_json(blob) render_blob_json(blob)
end end
format.js { render 'shared/snippets/show'}
end end
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
......
...@@ -6,6 +6,8 @@ class SnippetsController < ApplicationController ...@@ -6,6 +6,8 @@ class SnippetsController < ApplicationController
include RendersBlob include RendersBlob
include PreviewMarkdown include PreviewMarkdown
skip_before_action :verify_authenticity_token, only: [:show], if: :js_request?
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet # Allow read snippet
...@@ -77,6 +79,8 @@ class SnippetsController < ApplicationController ...@@ -77,6 +79,8 @@ class SnippetsController < ApplicationController
format.json do format.json do
render_blob_json(blob) render_blob_json(blob)
end end
format.js { render 'shared/snippets/show' }
end end
end end
......
...@@ -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
......
...@@ -43,6 +43,10 @@ module IconsHelper ...@@ -43,6 +43,10 @@ module IconsHelper
content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes) content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes)
end end
def external_snippet_icon(name)
content_tag(:span, "", class: "gl-snippet-icon gl-snippet-icon-#{name}")
end
def audit_icon(names, options = {}) def audit_icon(names, options = {})
case names case names
when "standard" when "standard"
......
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
...@@ -101,4 +101,39 @@ module SnippetsHelper ...@@ -101,4 +101,39 @@ module SnippetsHelper
# Return snippet with chunk array # Return snippet with chunk array
{ snippet_object: snippet, snippet_chunks: snippet_chunks } { snippet_object: snippet, snippet_chunks: snippet_chunks }
end end
def snippet_embed
"<script src=\"#{url_for(only_path: false, overwrite_params: nil)}.js\"></script>"
end
def embedded_snippet_raw_button
blob = @snippet.blob
return if blob.empty? || blob.raw_binary? || blob.stored_externally?
snippet_raw_url = if @snippet.is_a?(PersonalSnippet)
raw_snippet_url(@snippet)
else
raw_project_snippet_url(@snippet.project, @snippet)
end
link_to external_snippet_icon('doc_code'), snippet_raw_url, class: 'btn', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw'
end
def embedded_snippet_download_button
download_url = if @snippet.is_a?(PersonalSnippet)
raw_snippet_url(@snippet, inline: false)
else
raw_project_snippet_url(@snippet.project, @snippet, inline: false)
end
link_to external_snippet_icon('download'), download_url, class: 'btn', target: '_blank', title: 'Download', rel: 'noopener noreferrer'
end
def public_snippet?
if @snippet.project_id?
can?(nil, :read_project_snippet, @snippet)
else
can?(nil, :read_personal_snippet, @snippet)
end
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
...@@ -55,6 +57,8 @@ module CacheMarkdownField ...@@ -55,6 +57,8 @@ module CacheMarkdownField
# 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
......
...@@ -23,9 +23,12 @@ class InternalId < ActiveRecord::Base ...@@ -23,9 +23,12 @@ class InternalId < ActiveRecord::Base
# #
# The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL). # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
# As such, the increment is atomic and safe to be called concurrently. # As such, the increment is atomic and safe to be called concurrently.
def increment_and_save! #
# If a `maximum_iid` is passed in, this overrides the incremented value if it's
# greater than that. This can be used to correct the increment value if necessary.
def increment_and_save!(maximum_iid)
lock! lock!
self.last_value = (last_value || 0) + 1 self.last_value = [(last_value || 0) + 1, (maximum_iid || 0) + 1].max
save! save!
last_value last_value
end end
...@@ -89,7 +92,16 @@ class InternalId < ActiveRecord::Base ...@@ -89,7 +92,16 @@ class InternalId < ActiveRecord::Base
# and increment its last value # and increment its last value
# #
# Note this will acquire a ROW SHARE lock on the InternalId record # Note this will acquire a ROW SHARE lock on the InternalId record
(lookup || create_record).increment_and_save!
# Note we always calculate the maximum iid present here and
# pass it in to correct the InternalId entry if it's last_value is off.
#
# This can happen in a transition phase where both `AtomicInternalId` and
# `NonatomicInternalId` code runs (e.g. during a deploy).
#
# This is subject to be cleaned up with the 10.8 release:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/45389.
(lookup || create_record).increment_and_save!(maximum_iid)
end end
end end
...@@ -115,11 +127,15 @@ class InternalId < ActiveRecord::Base ...@@ -115,11 +127,15 @@ class InternalId < ActiveRecord::Base
InternalId.create!( InternalId.create!(
**scope, **scope,
usage: usage_value, usage: usage_value,
last_value: init.call(subject) || 0 last_value: maximum_iid
) )
end end
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
lookup lookup
end end
def maximum_iid
@maximum_iid ||= init.call(subject) || 0
end
end end
end end
...@@ -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)
......
...@@ -126,6 +126,7 @@ ...@@ -126,6 +126,7 @@
GitLab GitLab
%span.pull-right %span.pull-right
= Gitlab::VERSION = Gitlab::VERSION
= "(#{Gitlab::REVISION})"
%p %p
GitLab Shell GitLab Shell
%span.pull-right %span.pull-right
......
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?
......
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.
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