summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2018-04-18 14:19:11 +0100
committerPhil Hughes <me@iamphill.com>2018-04-18 14:19:11 +0100
commit64be67f322f0aa4acee8df5062631a6ee867f730 (patch)
tree3b733e113bee9b6b8c1bbc29c7dba00900bb4097 /app
parent48fb30f730f0bf8c02c21d992cb83fdf35a00e91 (diff)
parent9c2f6e04cf5d2eb89d0ec68e8ace87b3804e9ae5 (diff)
downloadgitlab-ce-64be67f322f0aa4acee8df5062631a6ee867f730.tar.gz
Merge branch 'master' into ide-file-finder
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/ext_snippet_icons/ext_snippet_icons.pngbin0 -> 1018 bytes
-rw-r--r--app/assets/images/ext_snippet_icons/logo.pngbin0 -> 494 bytes
-rw-r--r--app/assets/javascripts/branches/branches_delete_modal.js11
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue185
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight.js17
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_helper.js29
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue58
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue130
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue88
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue30
-rw-r--r--app/assets/javascripts/ide/constants.js4
-rw-r--r--app/assets/javascripts/ide/ide_router.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js100
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js8
-rw-r--r--app/assets/javascripts/milestone.js22
-rw-r--r--app/assets/javascripts/notes.js46
-rw-r--r--app/assets/javascripts/pages/dashboard/milestones/show/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/milestones/show/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/snippets/show/index.js6
-rw-r--r--app/assets/javascripts/pages/snippets/show/index.js10
-rw-r--r--app/assets/javascripts/performance_bar/services/performance_bar_service.js30
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue7
-rw-r--r--app/assets/javascripts/pipelines/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js39
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js5
-rw-r--r--app/assets/javascripts/shared/popover.js33
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js11
-rw-r--r--app/assets/javascripts/snippet/snippet_embed.js23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue (renamed from app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js)267
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue2
-rw-r--r--app/assets/stylesheets/application.scss6
-rw-r--r--app/assets/stylesheets/framework/animations.scss80
-rw-r--r--app/assets/stylesheets/framework/banner.scss19
-rw-r--r--app/assets/stylesheets/framework/buttons.scss46
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss3
-rw-r--r--app/assets/stylesheets/framework/snippets.scss27
-rw-r--r--app/assets/stylesheets/framework/variables.scss14
-rw-r--r--app/assets/stylesheets/highlight/embedded.scss3
-rw-r--r--app/assets/stylesheets/highlight/white.scss291
-rw-r--r--app/assets/stylesheets/highlight/white_base.scss290
-rw-r--r--app/assets/stylesheets/pages/commits.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss5
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss17
-rw-r--r--app/assets/stylesheets/pages/milestone.scss35
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss5
-rw-r--r--app/assets/stylesheets/pages/repo.scss76
-rw-r--r--app/assets/stylesheets/snippets.scss156
-rw-r--r--app/controllers/application_controller.rb1
-rw-r--r--app/controllers/concerns/notes_actions.rb4
-rw-r--r--app/controllers/concerns/snippets_actions.rb4
-rw-r--r--app/controllers/dashboard/todos_controller.rb2
-rw-r--r--app/controllers/groups/variables_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb4
-rw-r--r--app/controllers/projects/snippets_controller.rb3
-rw-r--r--app/controllers/projects/variables_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/controllers/snippets_controller.rb4
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/helpers/blob_helper.rb2
-rw-r--r--app/helpers/diff_helper.rb2
-rw-r--r--app/helpers/icons_helper.rb4
-rw-r--r--app/helpers/safe_params_helper.rb11
-rw-r--r--app/helpers/snippets_helper.rb35
-rw-r--r--app/helpers/tree_helper.rb2
-rw-r--r--app/mailers/emails/issues.rb6
-rw-r--r--app/models/ci/build.rb8
-rw-r--r--app/models/ci/group_variable.rb2
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/concerns/cache_markdown_field.rb30
-rw-r--r--app/models/internal_id.rb24
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/notification_recipient.rb12
-rw-r--r--app/models/notification_setting.rb3
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/repository.rb1
-rw-r--r--app/services/ci/register_job_service.rb23
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb2
-rw-r--r--app/services/clusters/gcp/verify_provision_status_service.rb2
-rw-r--r--app/services/create_deployment_service.rb4
-rw-r--r--app/services/import_export_clean_up_service.rb2
-rw-r--r--app/services/notification_recipient_service.rb11
-rw-r--r--app/services/notification_service.rb14
-rw-r--r--app/services/projects/destroy_service.rb2
-rw-r--r--app/services/repository_archive_clean_up_service.rb2
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/services/test_hooks/base_service.rb2
-rw-r--r--app/views/admin/dashboard/index.html.haml1
-rw-r--r--app/views/dashboard/issues.atom.builder2
-rw-r--r--app/views/dashboard/issues.html.haml4
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml7
-rw-r--r--app/views/groups/issues.atom.builder2
-rw-r--r--app/views/groups/issues.html.haml2
-rw-r--r--app/views/notify/issue_due_email.html.haml12
-rw-r--r--app/views/notify/issue_due_email.text.erb7
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml2
-rw-r--r--app/views/projects/blob/_viewer.html.haml5
-rw-r--r--app/views/projects/blob/viewers/_highlight_embed.html.haml7
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/clusters/new.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/diffs/_collapsed.html.haml2
-rw-r--r--app/views/projects/issues/_nav_btns.html.haml2
-rw-r--r--app/views/projects/issues/index.atom.builder2
-rw-r--r--app/views/projects/issues/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml8
-rw-r--r--app/views/projects/pipelines/new.html.haml15
-rw-r--r--app/views/projects/protected_branches/_create_protected_branch.html.haml4
-rw-r--r--app/views/projects/protected_branches/_update_protected_branch.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_branches_list.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_dropdown.html.haml4
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_protected_branch.html.haml2
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml8
-rw-r--r--app/views/shared/_ref_switcher.html.haml4
-rw-r--r--app/views/shared/milestones/_deprecation_message.html.haml14
-rw-r--r--app/views/shared/milestones/_top.html.haml13
-rw-r--r--app/views/shared/snippets/_embed.html.haml24
-rw-r--r--app/views/shared/snippets/_header.html.haml25
-rw-r--r--app/views/shared/snippets/show.js.haml2
-rw-r--r--app/workers/all_queues.yml3
-rw-r--r--app/workers/concerns/mail_scheduler_queue.rb7
-rw-r--r--app/workers/issue_due_scheduler_worker.rb10
-rw-r--r--app/workers/mail_scheduler/issue_due_worker.rb14
-rw-r--r--app/workers/post_receive.rb2
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb2
131 files changed, 1813 insertions, 968 deletions
diff --git a/app/assets/images/ext_snippet_icons/ext_snippet_icons.png b/app/assets/images/ext_snippet_icons/ext_snippet_icons.png
new file mode 100644
index 00000000000..20380adc4e5
--- /dev/null
+++ b/app/assets/images/ext_snippet_icons/ext_snippet_icons.png
Binary files differ
diff --git a/app/assets/images/ext_snippet_icons/logo.png b/app/assets/images/ext_snippet_icons/logo.png
new file mode 100644
index 00000000000..794c9cc2dbc
--- /dev/null
+++ b/app/assets/images/ext_snippet_icons/logo.png
Binary files differ
diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js
index 839e369eaf6..f34496f84c6 100644
--- a/app/assets/javascripts/branches/branches_delete_modal.js
+++ b/app/assets/javascripts/branches/branches_delete_modal.js
@@ -16,6 +16,7 @@ class DeleteModal {
bindEvents() {
this.$toggleBtns.on('click', this.setModalData.bind(this));
this.$confirmInput.on('input', this.setDeleteDisabled.bind(this));
+ this.$deleteBtn.on('click', this.setDisableDeleteButton.bind(this));
}
setModalData(e) {
@@ -30,6 +31,16 @@ class DeleteModal {
this.$deleteBtn.attr('disabled', e.currentTarget.value !== this.branchName);
}
+ setDisableDeleteButton(e) {
+ if (this.$deleteBtn.is('[disabled]')) {
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+
+ return true;
+ }
+
updateModal() {
this.$branchName.text(this.branchName);
this.$confirmInput.val('');
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index f8dcdf3f60a..9c12b89240c 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -1,96 +1,102 @@
<script>
- import _ from 'underscore';
- import { s__, sprintf } from '../../locale';
- import applicationRow from './application_row.vue';
- import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
- import {
- APPLICATION_INSTALLED,
- INGRESS,
- } from '../constants';
+import _ from 'underscore';
+import { s__, sprintf } from '../../locale';
+import applicationRow from './application_row.vue';
+import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import { APPLICATION_INSTALLED, INGRESS } from '../constants';
- export default {
- components: {
- applicationRow,
- clipboardButton,
+export default {
+ components: {
+ applicationRow,
+ clipboardButton,
+ },
+ props: {
+ applications: {
+ type: Object,
+ required: false,
+ default: () => ({}),
},
- props: {
- applications: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- helpPath: {
- type: String,
- required: false,
- default: '',
- },
- ingressHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- ingressDnsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- managePrometheusPath: {
- type: String,
- required: false,
- default: '',
- },
+ helpPath: {
+ type: String,
+ required: false,
+ default: '',
},
- computed: {
- generalApplicationDescription() {
- return sprintf(
- _.escape(s__(
+ ingressHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ ingressDnsHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ managePrometheusPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ generalApplicationDescription() {
+ return sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`,
- )), {
- helpLink: `<a href="${this.helpPath}">
+ ),
+ ),
+ {
+ helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`,
- },
- false,
- );
- },
- ingressId() {
- return INGRESS;
- },
- ingressInstalled() {
- return this.applications.ingress.status === APPLICATION_INSTALLED;
- },
- ingressExternalIp() {
- return this.applications.ingress.externalIp;
- },
- ingressDescription() {
- const extraCostParagraph = sprintf(
- _.escape(s__(
+ },
+ false,
+ );
+ },
+ ingressId() {
+ return INGRESS;
+ },
+ ingressInstalled() {
+ return this.applications.ingress.status === APPLICATION_INSTALLED;
+ },
+ ingressExternalIp() {
+ return this.applications.ingress.externalIp;
+ },
+ ingressDescription() {
+ const extraCostParagraph = sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources
like a load balancer, which may incur additional costs depending on
- the hosting provider your Kubernetes cluster is installed on. If you are using GKE,
- you can %{pricingLink}.`,
- )), {
- boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
- pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
+ the hosting provider your Kubernetes cluster is installed on. If you are using
+ Google Kubernetes Engine, you can %{pricingLink}.`,
+ ),
+ ),
+ {
+ boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
+ pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`,
- },
- false,
- );
+ },
+ false,
+ );
- const externalIpParagraph = sprintf(
- _.escape(s__(
+ const externalIpParagraph = sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS
at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`,
- )), {
- ingressHelpLink: `<a href="${this.ingressHelpPath}">
+ ),
+ ),
+ {
+ ingressHelpLink: `<a href="${this.ingressHelpPath}">
${_.escape(s__('ClusterIntegration|More information'))}
</a>`,
- },
- false,
- );
+ },
+ false,
+ );
- return `
+ return `
<p>
${extraCostParagraph}
</p>
@@ -98,22 +104,25 @@
${externalIpParagraph}
</p>
`;
- },
- prometheusDescription() {
- return sprintf(
- _.escape(s__(
+ },
+ prometheusDescription() {
+ return sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|Prometheus is an open-source monitoring system
with %{gitlabIntegrationLink} to monitor deployed applications.`,
- )), {
- gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
+ ),
+ ),
+ {
+ gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`,
- },
- false,
- );
- },
+ },
+ false,
+ );
},
- };
+ },
+};
</script>
<template>
@@ -205,7 +214,7 @@
>
{{ s__(`ClusterIntegration|The IP address is in
the process of being assigned. Please check your Kubernetes
- cluster or Quotas on GKE if it takes a long time.`) }}
+ cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }}
<a
:href="ingressHelpPath"
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js
index c50ac667c20..2d5bae9a9c4 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight.js
@@ -1,19 +1,19 @@
import $ from 'jquery';
-import _ from 'underscore';
import {
getSelector,
- togglePopover,
inserted,
- mouseenter,
- mouseleave,
} from './feature_highlight_helper';
+import {
+ togglePopover,
+ mouseenter,
+ debouncedMouseleave,
+} from '../shared/popover';
export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
const $selector = $(getSelector(id));
const $parent = $selector.parent();
const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
const hideOnScroll = togglePopover.bind($selector, false);
- const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
$selector
// Setup popover
@@ -29,13 +29,10 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
`,
})
.on('mouseenter', mouseenter)
- .on('mouseleave', debouncedMouseleave)
+ .on('mouseleave', debouncedMouseleave(debounceTimeout))
.on('inserted.bs.popover', inserted)
.on('show.bs.popover', () => {
- window.addEventListener('scroll', hideOnScroll);
- })
- .on('hide.bs.popover', () => {
- window.removeEventListener('scroll', hideOnScroll);
+ window.addEventListener('scroll', hideOnScroll, { once: true });
})
// Display feature highlight
.removeAttr('disabled');
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
index f480e72961c..d5b97ebb264 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
@@ -3,20 +3,10 @@ import axios from '../lib/utils/axios_utils';
import { __ } from '../locale';
import Flash from '../flash';
import LazyLoader from '../lazy_loader';
+import { togglePopover } from '../shared/popover';
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) {
axios.post(this.attr('data-dismiss-endpoint'), {
feature_name: highlightId,
@@ -27,23 +17,6 @@ export function dismiss(highlightId) {
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() {
const popoverId = this.getAttribute('aria-describedby');
const highlightId = this.dataset.highlight;
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 2cbd982af19..45321df191c 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,41 +1,27 @@
<script>
- import { mapState } from 'vuex';
- import { sprintf, __ } from '~/locale';
- import * as consts from '../../stores/modules/commit/constants';
- import RadioGroup from './radio_group.vue';
+import { mapState } from 'vuex';
+import { sprintf, __ } from '~/locale';
+import * as consts from '../../stores/modules/commit/constants';
+import RadioGroup from './radio_group.vue';
- export default {
- components: {
- RadioGroup,
+export default {
+ components: {
+ RadioGroup,
+ },
+ computed: {
+ ...mapState(['currentBranchId']),
+ commitToCurrentBranchText() {
+ return sprintf(
+ __('Commit to %{branchName} branch'),
+ { branchName: `<strong class="monospace">${this.currentBranchId}</strong>` },
+ false,
+ );
},
- computed: {
- ...mapState([
- 'currentBranchId',
- ]),
- newMergeRequestHelpText() {
- return sprintf(
- __('Creates a new branch from %{branchName} and re-directs to create a new merge request'),
- { branchName: this.currentBranchId },
- );
- },
- commitToCurrentBranchText() {
- return sprintf(
- __('Commit to %{branchName} branch'),
- { branchName: `<strong>${this.currentBranchId}</strong>` },
- false,
- );
- },
- commitToNewBranchText() {
- return sprintf(
- __('Creates a new branch from %{branchName}'),
- { branchName: this.currentBranchId },
- );
- },
- },
- commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
- commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
- commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
- };
+ },
+ commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
+ commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
+ commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
+};
</script>
<template>
@@ -53,13 +39,11 @@
:value="$options.commitToNewBranch"
:label="__('Create a new branch')"
:show-input="true"
- :help-text="commitToNewBranchText"
/>
<radio-group
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
:show-input="true"
- :help-text="newMergeRequestHelpText"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
new file mode 100644
index 00000000000..dcd934f76b7
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
@@ -0,0 +1,130 @@
+<script>
+import { __, sprintf } from '../../../locale';
+import Icon from '../../../vue_shared/components/icon.vue';
+import popover from '../../../vue_shared/directives/popover';
+import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
+
+export default {
+ directives: {
+ popover,
+ },
+ components: {
+ Icon,
+ },
+ props: {
+ text: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ scrollTop: 0,
+ isFocused: false,
+ };
+ },
+ computed: {
+ allLines() {
+ return this.text.split('\n').map((line, i) => ({
+ text: line.substr(0, this.getLineLength(i)) || ' ',
+ highlightedText: line.substr(this.getLineLength(i)),
+ }));
+ },
+ },
+ methods: {
+ handleScroll() {
+ if (this.$refs.textarea) {
+ this.$nextTick(() => {
+ this.scrollTop = this.$refs.textarea.scrollTop;
+ });
+ }
+ },
+ getLineLength(i) {
+ return i === 0 ? MAX_TITLE_LENGTH : MAX_BODY_LENGTH;
+ },
+ onInput(e) {
+ this.$emit('input', e.target.value);
+ },
+ updateIsFocused(isFocused) {
+ this.isFocused = isFocused;
+ },
+ },
+ popoverOptions: {
+ trigger: 'hover',
+ placement: 'top',
+ content: sprintf(
+ __(`
+ The character highligher helps you keep the subject line to %{titleLength} characters
+ and wrap the body at %{bodyLength} so they are readable in git.
+ `),
+ { titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
+ ),
+ },
+};
+</script>
+
+<template>
+ <fieldset class="common-note-form ide-commit-message-field">
+ <div
+ class="md-area"
+ :class="{
+ 'is-focused': isFocused
+ }"
+ >
+ <div
+ v-once
+ class="md-header"
+ >
+ <ul class="nav-links">
+ <li>
+ {{ __('Commit Message') }}
+ <span
+ v-popover="$options.popoverOptions"
+ class="help-block prepend-left-10"
+ >
+ <icon
+ name="question"
+ />
+ </span>
+ </li>
+ </ul>
+ </div>
+ <div class="ide-commit-message-textarea-container">
+ <div class="ide-commit-message-highlights-container">
+ <div
+ class="note-textarea highlights monospace"
+ :style="{
+ transform: `translate3d(0, ${-scrollTop}px, 0)`
+ }"
+ >
+ <div
+ v-for="(line, index) in allLines"
+ :key="index"
+ >
+ <span
+ v-text="line.text"
+ >
+ </span><mark
+ v-show="line.highlightedText"
+ v-text="line.highlightedText"
+ >
+ </mark>
+ </div>
+ </div>
+ </div>
+ <textarea
+ class="note-textarea ide-commit-message-textarea"
+ name="commit-message"
+ :placeholder="__('Write a commit message...')"
+ :value="text"
+ @scroll="handleScroll"
+ @input="onInput"
+ @focus="updateIsFocused(true)"
+ @blur="updateIsFocused(false)"
+ ref="textarea"
+ >
+ </textarea>
+ </div>
+ </div>
+ </fieldset>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index 4310d762c78..b660a2961cb 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -1,52 +1,40 @@
<script>
- import { mapActions, mapState, mapGetters } from 'vuex';
- import tooltip from '~/vue_shared/directives/tooltip';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import tooltip from '~/vue_shared/directives/tooltip';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ props: {
+ value: {
+ type: String,
+ required: true,
},
- props: {
- value: {
- type: String,
- required: true,
- },
- label: {
- type: String,
- required: false,
- default: null,
- },
- checked: {
- type: Boolean,
- required: false,
- default: false,
- },
- showInput: {
- type: Boolean,
- required: false,
- default: false,
- },
- helpText: {
- type: String,
- required: false,
- default: null,
- },
+ label: {
+ type: String,
+ required: false,
+ default: null,
},
- computed: {
- ...mapState('commit', [
- 'commitAction',
- ]),
- ...mapGetters('commit', [
- 'newBranchName',
- ]),
+ checked: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- methods: {
- ...mapActions('commit', [
- 'updateCommitAction',
- 'updateBranchName',
- ]),
+ showInput: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- };
+ },
+ computed: {
+ ...mapState('commit', ['commitAction']),
+ ...mapGetters('commit', ['newBranchName']),
+ },
+ methods: {
+ ...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
+ },
+};
</script>
<template>
@@ -65,18 +53,6 @@
{{ label }}
</template>
<slot v-else></slot>
- <span
- v-if="helpText"
- v-tooltip
- class="help-block inline"
- :title="helpText"
- >
- <i
- class="fa fa-question-circle"
- aria-hidden="true"
- >
- </i>
- </span>
</span>
</label>
<div
@@ -85,7 +61,7 @@
>
<input
type="text"
- class="form-control"
+ class="form-control monospace"
:placeholder="newBranchName"
@input="updateBranchName($event.target.value)"
/>
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index d885ed5e301..14673754503 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -5,6 +5,7 @@ import icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import commitFilesList from './commit_sidebar/list.vue';
+import CommitMessageField from './commit_sidebar/message_field.vue';
import * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue';
@@ -15,6 +16,7 @@ export default {
commitFilesList,
Actions,
LoadingButton,
+ CommitMessageField,
},
directives: {
tooltip,
@@ -38,15 +40,9 @@ export default {
'changedFiles',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
- ...mapGetters('commit', [
- 'commitButtonDisabled',
- 'discardDraftButtonDisabled',
- 'branchName',
- ]),
+ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']),
statusSvg() {
- return this.lastCommitMsg
- ? this.committedStateSvgPath
- : this.noChangesStateSvgPath;
+ return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
},
},
methods: {
@@ -64,9 +60,7 @@ export default {
});
},
forceCreateNewBranch() {
- return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() =>
- this.commitChanges(),
- );
+ return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
},
};
@@ -105,16 +99,10 @@ export default {
@submit.prevent.stop="commitChanges"
v-if="!rightPanelCollapsed"
>
- <div class="multi-file-commit-fieldset">
- <textarea
- class="form-control multi-file-commit-message"
- name="commit-message"
- :value="commitMessage"
- :placeholder="__('Write a commit message...')"
- @input="updateCommitMessage($event.target.value)"
- >
- </textarea>
- </div>
+ <commit-message-field
+ :text="commitMessage"
+ @input="updateCommitMessage"
+ />
<div class="clearfix prepend-top-15">
<actions />
<loading-button
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index e4e54f967aa..09ee9d39d43 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -6,3 +6,7 @@ export const UP_KEY_CODE = 38;
export const DOWN_KEY_CODE = 40;
export const ENTER_KEY_CODE = 13;
export const ESC_KEY_CODE = 27;
+
+// Commit message textarea
+export const MAX_TITLE_LENGTH = 50;
+export const MAX_BODY_LENGTH = 72;
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 20983666b4a..4a0a303d5a6 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -36,11 +36,11 @@ const router = new VueRouter({
base: `${gon.relative_url_root}/-/ide/`,
routes: [
{
- path: '/project/:namespace/:project',
+ path: '/project/:namespace/:project+',
component: EmptyRouterComponent,
children: [
{
- path: ':targetmode/:branch/*',
+ path: ':targetmode(edit|tree|blob)/:branch/*',
component: EmptyRouterComponent,
},
{
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index b3882cb8d21..4eb23b2ee0f 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -5,45 +5,71 @@ import * as types from '../mutation_types';
export const getProjectData = (
{ commit, state, dispatch },
{ namespace, projectId, force = false } = {},
-) => new Promise((resolve, reject) => {
- if (!state.projects[`${namespace}/${projectId}`] || force) {
- commit(types.TOGGLE_LOADING, { entry: state });
- service.getProjectData(namespace, projectId)
- .then(res => res.data)
- .then((data) => {
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, { entry: state });
- commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
- if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
- resolve(data);
- })
- .catch(() => {
- flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
- reject(new Error(`Project not loaded ${namespace}/${projectId}`));
- });
- } else {
- resolve(state.projects[`${namespace}/${projectId}`]);
- }
-});
+ service
+ .getProjectData(namespace, projectId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.TOGGLE_LOADING, { entry: state });
+ commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
+ if (!state.currentProjectId)
+ commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
+ resolve(data);
+ })
+ .catch(() => {
+ flash(
+ 'Error loading project data. Please try again.',
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ reject(new Error(`Project not loaded ${namespace}/${projectId}`));
+ });
+ } else {
+ resolve(state.projects[`${namespace}/${projectId}`]);
+ }
+ });
export const getBranchData = (
{ commit, state, dispatch },
{ projectId, branchId, force = false } = {},
-) => new Promise((resolve, reject) => {
- if ((typeof state.projects[`${projectId}`] === 'undefined' ||
- !state.projects[`${projectId}`].branches[branchId])
- || force) {
- service.getBranchData(`${projectId}`, branchId)
- .then(({ data }) => {
- const { id } = data.commit;
- commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
- commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
- resolve(data);
- })
- .catch(() => {
- flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
- reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
- });
- } else {
- resolve(state.projects[`${projectId}`].branches[branchId]);
- }
-});
+) =>
+ new Promise((resolve, reject) => {
+ if (
+ typeof state.projects[`${projectId}`] === 'undefined' ||
+ !state.projects[`${projectId}`].branches[branchId] ||
+ force
+ ) {
+ service
+ .getBranchData(`${projectId}`, branchId)
+ .then(({ data }) => {
+ const { id } = data.commit;
+ commit(types.SET_BRANCH, {
+ projectPath: `${projectId}`,
+ branchName: branchId,
+ branch: data,
+ });
+ commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
+ commit(types.SET_CURRENT_BRANCH, branchId);
+ resolve(data);
+ })
+ .catch(() => {
+ flash(
+ 'Error loading branch data. Please try again.',
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
+ });
+ } else {
+ resolve(state.projects[`${projectId}`].branches[branchId]);
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js
index 7f7e470c9bb..1176c040fb9 100644
--- a/app/assets/javascripts/ide/stores/mutations/tree.js
+++ b/app/assets/javascripts/ide/stores/mutations/tree.js
@@ -17,12 +17,8 @@ export default {
});
},
[types.SET_DIRECTORY_DATA](state, { data, treePath }) {
- Object.assign(state, {
- trees: Object.assign(state.trees, {
- [treePath]: {
- tree: data,
- },
- }),
+ Object.assign(state.trees[treePath], {
+ tree: data,
});
},
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index e6e3a66aa20..325fa570f37 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import flash from './flash';
+import { mouseenter, debouncedMouseleave, togglePopover } from './shared/popover';
export default class Milestone {
constructor() {
@@ -43,4 +44,25 @@ export default class Milestone {
.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 });
+ });
+ }
}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index b0573510ff9..2121907dff0 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -19,7 +19,6 @@ import AjaxCache from '~/lib/utils/ajax_cache';
import Vue from 'vue';
import syntaxHighlight from '~/syntax_highlight';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
@@ -198,6 +197,8 @@ export default class Notes {
);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
+ this.$wrapperEl.on('click', '.js-toggle-lazy-diff-retry-button', this.onClickRetryLazyLoad.bind(this));
+
// fetch notes when tab becomes visible
this.$wrapperEl.on('visibilitychange', this.visibilityChange);
// when issue status changes, we need to refresh data
@@ -244,6 +245,7 @@ export default class Notes {
this.$wrapperEl.off('click', '.js-comment-resolve-button');
this.$wrapperEl.off('click', '.system-note-commit-list-toggler');
this.$wrapperEl.off('click', '.js-toggle-lazy-diff');
+ this.$wrapperEl.off('click', '.js-toggle-lazy-diff-retry-button');
this.$wrapperEl.off('ajax:success', '.js-main-target-form');
this.$wrapperEl.off('ajax:success', '.js-discussion-note-form');
this.$wrapperEl.off('ajax:complete', '.js-main-target-form');
@@ -1431,16 +1433,15 @@ export default class Notes {
syntaxHighlight(fileHolder);
}
- static renderDiffError($container) {
- $container.find('.line_content').html(
- $(`
- <div class="nothing-here-block">
- ${__(
- 'Unable to load the diff.',
- )} <a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>?
- </div>
- `),
- );
+ onClickRetryLazyLoad(e) {
+ const $retryButton = $(e.currentTarget);
+
+ $retryButton.prop('disabled', true);
+
+ return this.loadLazyDiff(e)
+ .then(() => {
+ $retryButton.prop('disabled', false);
+ });
}
loadLazyDiff(e) {
@@ -1449,20 +1450,35 @@ export default class Notes {
$container.find('.js-toggle-lazy-diff').removeClass('js-toggle-lazy-diff');
- const tableEl = $container.find('tbody');
- if (tableEl.length === 0) return;
+ const $tableEl = $container.find('tbody');
+ if ($tableEl.length === 0) return;
const fileHolder = $container.find('.file-holder');
const url = fileHolder.data('linesPath');
- axios
+ const $errorContainer = $container.find('.js-error-lazy-load-diff');
+ const $successContainer = $container.find('.js-success-lazy-load');
+
+ /**
+ * We only fetch resolved discussions.
+ * Unresolved discussions don't have an endpoint being provided.
+ */
+ if (url) {
+ return axios
.get(url)
.then(({ data }) => {
+ // Reset state in case last request returned error
+ $successContainer.removeClass('hidden');
+ $errorContainer.addClass('hidden');
+
Notes.renderDiffContent($container, data);
})
.catch(() => {
- Notes.renderDiffError($container);
+ $successContainer.addClass('hidden');
+ $errorContainer.removeClass('hidden');
});
+ }
+ return Promise.resolve();
}
toggleCommitList(e) {
diff --git a/app/assets/javascripts/pages/dashboard/milestones/show/index.js b/app/assets/javascripts/pages/dashboard/milestones/show/index.js
index 397149aaa9e..8b529585898 100644
--- a/app/assets/javascripts/pages/dashboard/milestones/show/index.js
+++ b/app/assets/javascripts/pages/dashboard/milestones/show/index.js
@@ -6,4 +6,6 @@ document.addEventListener('DOMContentLoaded', () => {
new Milestone(); // eslint-disable-line no-new
new Sidebar(); // eslint-disable-line no-new
new MountMilestoneSidebar(); // eslint-disable-line no-new
+
+ Milestone.initDeprecationMessage();
});
diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js
index 88f40b5278e..74cc4ba42c1 100644
--- a/app/assets/javascripts/pages/groups/milestones/show/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/show/index.js
@@ -1,3 +1,8 @@
import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
+import Milestone from '~/milestone';
-document.addEventListener('DOMContentLoaded', initMilestonesShow);
+document.addEventListener('DOMContentLoaded', () => {
+ initMilestonesShow();
+
+ Milestone.initDeprecationMessage();
+});
diff --git a/app/assets/javascripts/pages/projects/snippets/show/index.js b/app/assets/javascripts/pages/projects/snippets/show/index.js
index a134599cb04..c35b9c30058 100644
--- a/app/assets/javascripts/pages/projects/snippets/show/index.js
+++ b/app/assets/javascripts/pages/projects/snippets/show/index.js
@@ -1,11 +1,13 @@
import initNotes from '~/init_notes';
import ZenMode from '~/zen_mode';
-import LineHighlighter from '../../../../line_highlighter';
-import BlobViewer from '../../../../blob/viewer';
+import LineHighlighter from '~/line_highlighter';
+import BlobViewer from '~/blob/viewer';
+import snippetEmbed from '~/snippet/snippet_embed';
document.addEventListener('DOMContentLoaded', () => {
new LineHighlighter(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new
initNotes();
new ZenMode(); // eslint-disable-line no-new
+ snippetEmbed();
});
diff --git a/app/assets/javascripts/pages/snippets/show/index.js b/app/assets/javascripts/pages/snippets/show/index.js
index f548b9fad65..26936110402 100644
--- a/app/assets/javascripts/pages/snippets/show/index.js
+++ b/app/assets/javascripts/pages/snippets/show/index.js
@@ -1,11 +1,13 @@
-import LineHighlighter from '../../../line_highlighter';
-import BlobViewer from '../../../blob/viewer';
-import ZenMode from '../../../zen_mode';
-import initNotes from '../../../init_notes';
+import LineHighlighter from '~/line_highlighter';
+import BlobViewer from '~/blob/viewer';
+import ZenMode from '~/zen_mode';
+import initNotes from '~/init_notes';
+import snippetEmbed from '~/snippet/snippet_embed';
document.addEventListener('DOMContentLoaded', () => {
new LineHighlighter(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new
initNotes();
new ZenMode(); // eslint-disable-line no-new
+ snippetEmbed();
});
diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
index 3ebfaa87a4e..bc71911ae35 100644
--- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js
+++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
@@ -10,29 +10,25 @@ export default class PerformanceBarService {
}
static registerInterceptor(peekUrl, callback) {
- vueResourceInterceptor = (request, next) => {
- next(response => {
- const requestId = response.headers['x-request-id'];
- const requestUrl = response.url;
-
- if (requestUrl !== peekUrl && requestId) {
- callback(requestId, requestUrl);
- }
- });
- };
-
- Vue.http.interceptors.push(vueResourceInterceptor);
-
- return axios.interceptors.response.use(response => {
+ const interceptor = response => {
const requestId = response.headers['x-request-id'];
- const requestUrl = response.config.url;
+ // Get the request URL from response.config for Axios, and response for
+ // Vue Resource.
+ const requestUrl = (response.config || response).url;
+ const cachedResponse = response.headers['x-gitlab-from-cache'] === 'true';
- if (requestUrl !== peekUrl && requestId) {
+ if (requestUrl !== peekUrl && requestId && !cachedResponse) {
callback(requestId, requestUrl);
}
return response;
- });
+ };
+
+ vueResourceInterceptor = (request, next) => next(interceptor);
+
+ Vue.http.interceptors.push(vueResourceInterceptor);
+
+ return axios.interceptors.response.use(interceptor);
}
static removeInterceptor(interceptor) {
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index b3fcaf0ccd1..32cf3dba3c3 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -1,5 +1,4 @@
<script>
- import $ from 'jquery';
/**
* Renders each stage of the pipeline mini graph.
@@ -13,8 +12,11 @@
* 3. Merge request widget
* 4. Commit widget
*/
- import axios from '../../lib/utils/axios_utils';
+
+ import $ from 'jquery';
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 LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -82,6 +84,7 @@
methods: {
onClickStage() {
if (!this.isDropdownOpen()) {
+ eventHub.$emit('clickedDropdown');
this.isLoading = true;
this.fetchJobs();
}
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
new file mode 100644
index 00000000000..b384c7500e7
--- /dev/null
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
+export const CANCEL_REQUEST = 'CANCEL_REQUEST';
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 522a4277bd7..6d87f75ae8e 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -7,6 +7,7 @@ import SvgBlankState from '../components/blank_state.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import PipelinesTableComponent from '../components/pipelines_table.vue';
import eventHub from '../event_hub';
+import { CANCEL_REQUEST } from '../constants';
export default {
components: {
@@ -52,34 +53,58 @@ export default {
});
eventHub.$on('postAction', this.postAction);
+ eventHub.$on('clickedDropdown', this.updateTable);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
+ eventHub.$off('clickedDropdown', this.updateTable);
},
destroyed() {
this.poll.stop();
},
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() {
if (!this.isMakingRequest) {
this.isLoading = true;
- this.service.getPipelines(this.requestData)
- .then(response => this.successCallback(response))
- .catch(() => this.errorCallback());
+ this.getPipelines();
}
},
+ getPipelines() {
+ return this.service.getPipelines(this.requestData)
+ .then(response => this.successCallback(response))
+ .catch((error) => this.errorCallback(error));
+ },
setCommonData(pipelines) {
this.store.storePipelines(pipelines);
this.isLoading = false;
this.updateGraphDropdown = 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() {
- this.hasError = true;
- this.isLoading = false;
- this.updateGraphDropdown = false;
+ errorCallback(error) {
this.hasMadeRequest = true;
+ this.isLoading = false;
+
+ if (error && error.message && error.message !== CANCEL_REQUEST) {
+ this.hasError = true;
+ this.updateGraphDropdown = false;
+ }
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index 001286f5d52..59c8b9c58e5 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -19,8 +19,13 @@ export default class PipelinesService {
getPipelines(data = {}) {
const { scope, page } = data;
+ const CancelToken = axios.CancelToken;
+
+ this.cancelationSource = CancelToken.source();
+
return axios.get(this.endpoint, {
params: { scope, page },
+ cancelToken: this.cancelationSource.token,
});
}
diff --git a/app/assets/javascripts/shared/popover.js b/app/assets/javascripts/shared/popover.js
new file mode 100644
index 00000000000..3fc03553bdd
--- /dev/null
+++ b/app/assets/javascripts/shared/popover.js
@@ -0,0 +1,33 @@
+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);
+}
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index 25f39e4fdb6..9f69f110d06 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -1,12 +1,15 @@
+import { visitUrl } from './lib/utils/url_utility';
+
/**
* Helper function that finds the href of the fiven selector and updates the location.
*
* @param {String} selector
*/
-export default (selector) => {
- const link = document.querySelector(selector).getAttribute('href');
+export default function findAndFollowLink(selector) {
+ const element = document.querySelector(selector);
+ const link = element && element.getAttribute('href');
if (link) {
- window.location = link;
+ visitUrl(link);
}
-};
+}
diff --git a/app/assets/javascripts/snippet/snippet_embed.js b/app/assets/javascripts/snippet/snippet_embed.js
new file mode 100644
index 00000000000..81ec483f2d9
--- /dev/null
+++ b/app/assets/javascripts/snippet/snippet_embed.js
@@ -0,0 +1,23 @@
+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';
+ });
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
deleted file mode 100644
index 4d9a2ca530f..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import statusIcon from '../mr_widget_status_icon.vue';
-
-export default {
- name: 'MRWidgetPipelineBlocked',
- components: {
- statusIcon,
- },
- template: `
- <div class="mr-widget-body media">
- <status-icon status="warning" :show-disabled-button="true" />
- <div class="media-body space-children">
- <span class="bold">
- The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
- </span>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
new file mode 100644
index 00000000000..8d55477929f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
@@ -0,0 +1,25 @@
+<script>
+import statusIcon from '../mr_widget_status_icon.vue';
+
+export default {
+ name: 'PipelineFailed',
+ components: {
+ statusIcon,
+ },
+};
+</script>
+
+<template>
+ <div class="mr-widget-body media">
+ <status-icon
+ status="warning"
+ :show-disabled-button="true"
+ />
+ <div class="media-body space-children">
+ <span class="bold">
+ {{ s__(`mrWidget|The pipeline for this merge request failed.
+Please retry the job or push a new commit to fix the failure`) }}
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 3c781ccddc8..0264625a526 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -1,3 +1,4 @@
+<script>
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
@@ -7,7 +8,10 @@ import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
export default {
- name: 'MRWidgetReadyToMerge',
+ name: 'ReadyToMerge',
+ components: {
+ statusIcon,
+ },
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
@@ -26,9 +30,6 @@ export default {
warningSvg,
};
},
- components: {
- statusIcon,
- },
computed: {
shouldShowMergeWhenPipelineSucceedsText() {
return this.mr.isPipelineActive;
@@ -217,136 +218,146 @@ export default {
});
},
},
- template: `
- <div class="mr-widget-body media">
- <status-icon :status="iconClass" />
- <div class="media-body">
- <div class="mr-widget-body-controls media space-children">
- <span class="btn-group append-bottom-5">
- <button
- @click="handleMergeButtonClick()"
- :disabled="isMergeButtonDisabled"
- :class="mergeButtonClass"
- type="button"
- class="qa-merge-button">
- <i
- v-if="isMakingRequest"
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- {{mergeButtonText}}
- </button>
+};
+</script>
+
+<template>
+ <div class="mr-widget-body media">
+ <status-icon :status="iconClass" />
+ <div class="media-body">
+ <div class="mr-widget-body-controls media space-children">
+ <span class="btn-group append-bottom-5">
+ <button
+ @click="handleMergeButtonClick()"
+ :disabled="isMergeButtonDisabled"
+ :class="mergeButtonClass"
+ type="button"
+ class="qa-merge-button">
+ <i
+ v-if="isMakingRequest"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"
+ ></i>
+ {{ mergeButtonText }}
+ </button>
+ <button
+ v-if="shouldShowMergeOptionsDropdown"
+ :disabled="isMergeButtonDisabled"
+ type="button"
+ class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
+ data-toggle="dropdown"
+ aria-label="Select merge moment">
+ <i
+ class="fa fa-chevron-down"
+ aria-hidden="true"
+ ></i>
+ </button>
+ <ul
+ v-if="shouldShowMergeOptionsDropdown"
+ class="dropdown-menu dropdown-menu-right"
+ role="menu">
+ <li>
+ <a
+ @click.prevent="handleMergeButtonClick(true)"
+ class="merge_when_pipeline_succeeds"
+ href="#">
+ <span class="media">
+ <span
+ v-html="successSvg"
+ class="merge-opt-icon"
+ aria-hidden="true"></span>
+ <span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a
+ @click.prevent="handleMergeButtonClick(false, true)"
+ class="accept-merge-request"
+ href="#">
+ <span class="media">
+ <span
+ v-html="warningSvg"
+ class="merge-opt-icon"
+ aria-hidden="true"></span>
+ <span class="media-body merge-opt-title">Merge immediately</span>
+ </span>
+ </a>
+ </li>
+ </ul>
+ </span>
+ <div class="media-body-wrap space-children">
+ <template v-if="shouldShowMergeControls()">
+ <label v-if="mr.canRemoveSourceBranch">
+ <input
+ id="remove-source-branch-input"
+ v-model="removeSourceBranch"
+ class="js-remove-source-branch-checkbox"
+ :disabled="isRemoveSourceBranchButtonDisabled"
+ type="checkbox"/> Remove source branch
+ </label>
+
+ <!-- Placeholder for EE extension of this component -->
+ <squash-before-merge
+ v-if="shouldShowSquashBeforeMerge"
+ :mr="mr"
+ :is-merge-button-disabled="isMergeButtonDisabled" />
+
+ <span
+ v-if="mr.ffOnlyEnabled"
+ class="js-fast-forward-message">
+ Fast-forward merge without a merge commit
+ </span>
<button
- v-if="shouldShowMergeOptionsDropdown"
+ v-else
+ @click="toggleCommitMessageEditor"
:disabled="isMergeButtonDisabled"
- type="button"
- class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
- data-toggle="dropdown"
- aria-label="Select merge moment">
- <i
- class="fa fa-chevron-down"
- aria-hidden="true" />
+ class="js-modify-commit-message-button btn btn-default btn-xs"
+ type="button">
+ Modify commit message
</button>
- <ul
- v-if="shouldShowMergeOptionsDropdown"
- class="dropdown-menu dropdown-menu-right"
- role="menu">
- <li>
- <a
- @click.prevent="handleMergeButtonClick(true)"
- class="merge_when_pipeline_succeeds"
- href="#">
- <span class="media">
- <span
- v-html="successSvg"
- class="merge-opt-icon"
- aria-hidden="true"></span>
- <span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
- </span>
- </a>
- </li>
- <li>
- <a
- @click.prevent="handleMergeButtonClick(false, true)"
- class="accept-merge-request"
- href="#">
- <span class="media">
- <span
- v-html="warningSvg"
- class="merge-opt-icon"
- aria-hidden="true"></span>
- <span class="media-body merge-opt-title">Merge immediately</span>
- </span>
- </a>
- </li>
- </ul>
- </span>
- <div class="media-body-wrap space-children">
- <template v-if="shouldShowMergeControls()">
- <label v-if="mr.canRemoveSourceBranch">
- <input
- id="remove-source-branch-input"
- v-model="removeSourceBranch"
- class="js-remove-source-branch-checkbox"
- :disabled="isRemoveSourceBranchButtonDisabled"
- type="checkbox"/> Remove source branch
- </label>
-
- <!-- Placeholder for EE extension of this component -->
- <squash-before-merge
- v-if="shouldShowSquashBeforeMerge"
- :mr="mr"
- :is-merge-button-disabled="isMergeButtonDisabled" />
-
- <span
- v-if="mr.ffOnlyEnabled"
- class="js-fast-forward-message">
- Fast-forward merge without a merge commit
- </span>
- <button
- v-else
- @click="toggleCommitMessageEditor"
- :disabled="isMergeButtonDisabled"
- class="js-modify-commit-message-button btn btn-default btn-xs"
- type="button">
- Modify commit message
- </button>
- </template>
- <template v-else>
- <span class="bold js-resolve-mr-widget-items-message">
- You can only merge once the items above are resolved
- </span>
- </template>
- </div>
+ </template>
+ <template v-else>
+ <span class="bold js-resolve-mr-widget-items-message">
+ You can only merge once the items above are resolved
+ </span>
+ </template>
</div>
- <div
- v-if="showCommitMessageEditor"
- class="prepend-top-default commit-message-editor">
- <div class="form-group clearfix">
- <label
- class="control-label"
- for="commit-message">
- Commit message
- </label>
- <div class="col-sm-10">
- <div class="commit-message-container">
- <div class="max-width-marker"></div>
- <textarea
- v-model="commitMessage"
- class="form-control js-commit-message"
- required="required"
- rows="14"
- name="Commit message"></textarea>
- </div>
- <p class="hint">Try to keep the first line under 52 characters and the others under 72</p>
- <div class="hint">
- <a
- @click.prevent="updateCommitMessage"
- href="#">{{commitMessageLinkTitle}}</a>
- </div>
+ </div>
+ <div
+ v-if="showCommitMessageEditor"
+ class="prepend-top-default commit-message-editor">
+ <div class="form-group clearfix">
+ <label
+ class="control-label"
+ for="commit-message">
+ Commit message
+ </label>
+ <div class="col-sm-10">
+ <div class="commit-message-container">
+ <div class="max-width-marker"></div>
+ <textarea
+ id="commit-message"
+ v-model="commitMessage"
+ class="form-control js-commit-message"
+ required="required"
+ rows="14"
+ name="Commit message"></textarea>
+ </div>
+ <p class="hint">
+ Try to keep the first line under 52 characters and the others under 72
+ </p>
+ <div class="hint">
+ <a
+ @click.prevent="updateCommitMessage"
+ href="#"
+ >
+ {{ commitMessageLinkTitle }}
+ </a>
</div>
</div>
</div>
</div>
</div>
- `,
-};
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index ed15fc6ab0f..3b5c973e4a0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -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 MissingBranchState } from './components/states/mr_widget_missing_branch.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 UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
-export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
+export { default as PipelineFailedState } from './components/states/pipeline_failed.vue';
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue';
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 97789636787..b8875d04488 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -175,7 +175,7 @@
</a>
</span>
<span v-else>
- Cant find HEAD commit for this branch
+ Can't find HEAD commit for this branch
</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
index b06493e6c66..16304e4815d 100644
--- a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
+++ b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
@@ -9,7 +9,7 @@
lines: {
type: Number,
required: false,
- default: 6,
+ default: 3,
},
},
computed: {
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 0665622fe4a..f2950308019 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -37,7 +37,11 @@
/*
* 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.
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 728f9a27aca..14cd32da9eb 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -187,12 +187,9 @@ a {
animation: fadeInFull $fade-in-duration 1;
}
-
.animation-container {
- background: $repo-editor-grey;
height: 40px;
overflow: hidden;
- position: relative;
&.animation-container-small {
height: 12px;
@@ -205,60 +202,43 @@ a {
}
}
- &::before {
- 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%;
+ [class^="skeleton-line-"] {
position: relative;
- }
-
- div {
- background: $white-light;
- height: 6px;
- left: 0;
- position: absolute;
- right: 0;
- }
-
- .skeleton-line-1 {
- left: 0;
- top: 8px;
- }
-
- .skeleton-line-2 {
- left: 150px;
- top: 0;
+ background-color: $theme-gray-100;
height: 10px;
- }
+ overflow: hidden;
- .skeleton-line-3 {
- left: 0;
- top: 23px;
- }
+ &:not(:last-of-type) {
+ margin-bottom: 4px;
+ }
- .skeleton-line-4 {
- left: 0;
- top: 38px;
+ &::after {
+ content: ' ';
+ 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;
+ }
}
+}
- .skeleton-line-5 {
- left: 200px;
- top: 28px;
- height: 10px;
- }
+$skeleton-line-widths: (
+ 156px,
+ 235px,
+ 200px,
+);
- .skeleton-line-6 {
- top: 14px;
- left: 230px;
- height: 10px;
+@for $count from 1 through length($skeleton-line-widths) {
+ .skeleton-line-#{$count} {
+ width: nth($skeleton-line-widths, $count);
}
}
diff --git a/app/assets/stylesheets/framework/banner.scss b/app/assets/stylesheets/framework/banner.scss
index 6433b0c7855..02f3896d591 100644
--- a/app/assets/stylesheets/framework/banner.scss
+++ b/app/assets/stylesheets/framework/banner.scss
@@ -1,7 +1,7 @@
.banner-callout {
display: flex;
position: relative;
- flex-wrap: wrap;
+ align-items: start;
.banner-close {
position: absolute;
@@ -16,10 +16,25 @@
}
.banner-graphic {
- margin: 20px auto;
+ margin: 0 $gl-padding $gl-padding 0;
}
&.banner-non-empty-state {
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;
+ }
+ }
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 6b89387ab5f..f4f5926e198 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -422,25 +422,43 @@
}
}
-.btn-link.btn-secondary-hover-link {
- color: $gl-text-color-secondary;
+.btn-link {
+ padding: 0;
+ background-color: transparent;
+ color: $blue-600;
+ font-weight: normal;
+ border-radius: 0;
+ border-color: transparent;
&:hover,
&:active,
&:focus {
- color: $gl-link-color;
- text-decoration: none;
+ color: $blue-800;
+ text-decoration: underline;
+ background-color: transparent;
+ border-color: transparent;
}
-}
-.btn-link.btn-primary-hover-link {
- color: inherit;
+ &.btn-secondary-hover-link {
+ color: $gl-text-color-secondary;
- &:hover,
- &:active,
- &:focus {
- color: $gl-link-color;
- text-decoration: none;
+ &:hover,
+ &:active,
+ &:focus {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
+ }
+
+ &.btn-primary-hover-link {
+ color: inherit;
+
+ &:hover,
+ &:active,
+ &:focus {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
}
}
@@ -485,3 +503,7 @@ fieldset[disabled] .btn,
@extend %disabled;
}
}
+
+.btn-no-padding {
+ padding: 0;
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index a8f7caca339..6952f356128 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -485,7 +485,8 @@
.dropdown-menu-selectable {
li {
- a {
+ a,
+ button {
padding: 8px 40px;
position: relative;
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index 30c15c231d5..606d4675f19 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -29,8 +29,10 @@
}
.snippet-title {
- font-size: 24px;
+ color: $gl-text-color;
+ font-size: 2em;
font-weight: $gl-font-weight-bold;
+ min-height: $header-height;
}
.snippet-edited-ago {
@@ -46,3 +48,26 @@
.snippet-scope-menu .btn-new {
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;
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 8ee1bb03d55..37223175199 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -714,20 +714,6 @@ $color-average-score: $orange-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
*/
$perf-bar-text: #999;
diff --git a/app/assets/stylesheets/highlight/embedded.scss b/app/assets/stylesheets/highlight/embedded.scss
new file mode 100644
index 00000000000..44c8a1d39ec
--- /dev/null
+++ b/app/assets/stylesheets/highlight/embedded.scss
@@ -0,0 +1,3 @@
+.code {
+ @import "white_base";
+}
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index c3d8f0c61a2..355c8d223f7 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,292 +1,3 @@
-/* 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 {
- // 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; }
+ @import "white_base";
}
diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss
new file mode 100644
index 00000000000..8cc5252648d
--- /dev/null
+++ b/app/assets/stylesheets/highlight/white_base.scss
@@ -0,0 +1,290 @@
+/* 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; }
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 86cdda0359e..e9384d41e00 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -180,10 +180,6 @@
justify-content: space-between;
align-items: center;
flex-grow: 1;
-
- .merge-request-branches & {
- flex-direction: column;
- }
}
.commit-content {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 7f037582ca0..11052be40a8 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -160,6 +160,11 @@
}
}
}
+
+ .diff-loading-error-block {
+ padding: $gl-padding * 2 $gl-padding;
+ text-align: center;
+ }
}
.image {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 4692d0fb873..66db4917178 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -762,3 +762,20 @@
max-width: 100%;
}
}
+
+// Hack alert: we've rewritten `btn` class in a way that
+// we've broken it and it is not possible to use with `btn-link`
+// which causes a blank button when it's disabled and hovering
+// The css in here is the boostrap one
+.btn-link-retry {
+ &[disabled] {
+ cursor: not-allowed;
+ box-shadow: none;
+ opacity: .65;
+
+ &:hover {
+ color: $file-mode-changed;
+ text-decoration: none;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index e5afa8fffcb..3af8d80daab 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -194,3 +194,38 @@
.issuable-row {
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;
+ }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 2c840cb407a..855ebf7d86d 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -14,6 +14,11 @@
.commit-title {
margin: 0;
+ white-space: normal;
+
+ @media (max-width: $screen-sm-max) {
+ justify-content: flex-end;
+ }
}
.ci-table {
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index f826f934abf..5381bdc2105 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -663,11 +663,6 @@
}
}
-.multi-file-commit-message.form-control {
- height: 160px;
- resize: none;
-}
-
.dirty-diff {
// !important need to override monaco inline style
width: 4px !important;
@@ -860,3 +855,74 @@
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;
+}
diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss
new file mode 100644
index 00000000000..0d6b0735f70
--- /dev/null
+++ b/app/assets/stylesheets/snippets.scss
@@ -0,0 +1,156 @@
+@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;
+ }
+ }
+ }
+}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 24651dd392c..0fdd4d2cb47 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -5,6 +5,7 @@ class ApplicationController < ActionController::Base
include Gitlab::GonHelper
include GitlabRoutingHelper
include PageLayoutHelper
+ include SafeParamsHelper
include SentryHelper
include WorkhorseHelper
include EnforcesTwoFactorAuthentication
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index ad4e936a3d4..0c34e49206a 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -217,7 +217,7 @@ module NotesActions
def note_project
strong_memoize(:note_project) do
- return nil unless project
+ next nil unless project
note_project_id = params[:note_project_id]
@@ -228,7 +228,7 @@ module NotesActions
project
end
- return access_denied! unless can?(current_user, :create_note, the_project)
+ next access_denied! unless can?(current_user, :create_note, the_project)
the_project
end
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
index 9095cc7f783..120614739aa 100644
--- a/app/controllers/concerns/snippets_actions.rb
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -17,6 +17,10 @@ module SnippetsActions
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
+ def js_request?
+ request.format.js?
+ end
+
private
def convert_line_endings(content)
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index e89eaf7edda..f9e8fe624e8 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -86,7 +86,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
out_of_range = todos.current_page > total_pages
if out_of_range
- redirect_to url_for(params.merge(page: total_pages, only_path: true))
+ redirect_to url_for(safe_params.merge(page: total_pages, only_path: true))
end
out_of_range
diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb
index 6142e75b4c1..4d8a20de017 100644
--- a/app/controllers/groups/variables_controller.rb
+++ b/app/controllers/groups/variables_controller.rb
@@ -15,7 +15,7 @@ module Groups
def update
if @group.update(group_variables_params)
respond_to do |format|
- format.json { return render_group_variables }
+ format.json { render_group_variables }
end
else
respond_to do |format|
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 5ac4b8710e2..79fa5818359 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -189,6 +189,6 @@ class GroupsController < Groups::ApplicationController
params[:id] = group.to_param
- url_for(params)
+ url_for(safe_params)
end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 54e7d81de6a..62b739918e6 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -60,13 +60,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
format.patch do
- return render_404 unless @merge_request.diff_refs
+ break render_404 unless @merge_request.diff_refs
send_git_patch @project.repository, @merge_request.diff_refs
end
format.diff do
- return render_404 unless @merge_request.diff_refs
+ break render_404 unless @merge_request.diff_refs
send_git_diff @project.repository, @merge_request.diff_refs
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 7c19aa7bb23..208a1d19862 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -5,6 +5,8 @@ class Projects::SnippetsController < Projects::ApplicationController
include SnippetsActions
include RendersBlob
+ skip_before_action :verify_authenticity_token, only: [:show], if: :js_request?
+
before_action :check_snippets_available!
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
@@ -71,6 +73,7 @@ class Projects::SnippetsController < Projects::ApplicationController
format.json do
render_blob_json(blob)
end
+ format.js { render 'shared/snippets/show'}
end
end
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 517d0b026c2..bf09ea7e4d8 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -12,7 +12,7 @@ class Projects::VariablesController < Projects::ApplicationController
def update
if @project.update(variables_params)
respond_to do |format|
- format.json { return render_variables }
+ format.json { render_variables }
end
else
respond_to do |format|
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 37f14230196..a93b116c6fe 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -404,7 +404,7 @@ class ProjectsController < Projects::ApplicationController
params[:namespace_id] = project.namespace.to_param
params[:id] = project.to_param
- url_for(params)
+ url_for(safe_params)
end
def project_export_enabled
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index be2d3f638ff..3d51520ddf4 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -6,6 +6,8 @@ class SnippetsController < ApplicationController
include RendersBlob
include PreviewMarkdown
+ skip_before_action :verify_authenticity_token, only: [:show], if: :js_request?
+
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet
@@ -77,6 +79,8 @@ class SnippetsController < ApplicationController
format.json do
render_blob_json(blob)
end
+
+ format.js { render 'shared/snippets/show' }
end
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 956df4a0a16..31f47a7aa7c 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -146,6 +146,6 @@ class UsersController < ApplicationController
end
def build_canonical_path(user)
- url_for(params.merge(username: user.to_param))
+ url_for(safe_params.merge(username: user.to_param))
end
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 866b8773db6..fef29789832 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -259,7 +259,7 @@ module BlobHelper
options = []
if error == :collapsed
- options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, expanded: true, format: nil)))
+ options << link_to('load it anyway', url_for(safe_params.merge(viewer: viewer.type, expanded: true, format: nil)))
end
# If the error is `:server_side_but_stored_externally`, the simple viewer will show the same error,
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index b5ca39711bc..1bb82fd8150 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -180,7 +180,7 @@ module DiffHelper
private
def diff_btn(title, name, selected)
- params_copy = params.dup
+ params_copy = safe_params.dup
params_copy[:view] = name
# Always use HTML to handle case where JSON diff rendered this button
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index c5522ff7a69..2f304b040c7 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -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)
end
+ def external_snippet_icon(name)
+ content_tag(:span, "", class: "gl-snippet-icon gl-snippet-icon-#{name}")
+ end
+
def audit_icon(names, options = {})
case names
when "standard"
diff --git a/app/helpers/safe_params_helper.rb b/app/helpers/safe_params_helper.rb
new file mode 100644
index 00000000000..b568e8810cc
--- /dev/null
+++ b/app/helpers/safe_params_helper.rb
@@ -0,0 +1,11 @@
+module SafeParamsHelper
+ # Rails 5.0 requires to permit `params` if they're used in url helpers.
+ # Use this helper when generating links with `params.merge(...)`
+ def safe_params
+ if params.respond_to?(:permit!)
+ params.except(:host, :port, :protocol).permit!
+ else
+ params
+ end
+ end
+end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 00e7e4230b9..733832c1bbb 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -101,4 +101,39 @@ module SnippetsHelper
# Return snippet with chunk array
{ snippet_object: snippet, snippet_chunks: snippet_chunks }
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
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 5e7c20ef51e..dc42caa70e5 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -90,7 +90,7 @@ module TreeHelper
end
def commit_in_single_accessible_branch
- branch_name = html_escape(selected_branch)
+ branch_name = ERB::Util.html_escape(selected_branch)
message = _("Your changes can be committed to %{branch_name} because a merge "\
"request is open.") % { branch_name: "<strong>#{branch_name}</strong>" }
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index b33131becd3..392cc0bee03 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -6,6 +6,12 @@ module Emails
mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason))
end
+ def issue_due_email(recipient_id, issue_id, reason = nil)
+ setup_issue_mail(issue_id, recipient_id)
+
+ mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason))
+ end
+
def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 4aa65bf4273..1c8d9ca4aa5 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -162,7 +162,7 @@ module Ci
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
end
- before_transition pending: :running do |build|
+ after_transition pending: :running do |build|
build.ensure_metadata.update_timeout_state
end
end
@@ -479,7 +479,7 @@ module Ci
def user_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- return variables if user.blank?
+ break variables if user.blank?
variables.append(key: 'GITLAB_USER_ID', value: user.id.to_s)
variables.append(key: 'GITLAB_USER_EMAIL', value: user.email)
@@ -594,7 +594,7 @@ module Ci
def persisted_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- return variables unless persisted?
+ break variables unless persisted?
variables
.append(key: 'CI_JOB_ID', value: id.to_s)
@@ -643,7 +643,7 @@ module Ci
def persisted_environment_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- return variables unless persisted? && persisted_environment.present?
+ break variables unless persisted? && persisted_environment.present?
variables.concat(persisted_environment.predefined_variables)
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index 62d768cc6cf..44cb583e1bd 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -4,7 +4,7 @@ module Ci
include HasVariable
include Presentable
- belongs_to :group
+ belongs_to :group, class_name: "::Group"
alias_attribute :secret_value, :value
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 3469d5d795c..b6276c2fb50 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -87,7 +87,7 @@ class CommitStatus < ActiveRecord::Base
transition [:created, :pending, :running, :manual] => :canceled
end
- before_transition created: [:pending, :running] do |commit_status|
+ before_transition [:created, :skipped, :manual] => :pending do |commit_status|
commit_status.queued_at = Time.now
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 4ae5dd8c677..db8cf322ef7 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -11,7 +11,9 @@ module CacheMarkdownField
extend ActiveSupport::Concern
# Increment this number every time the renderer changes its output
- CACHE_VERSION = 3
+ CACHE_REDCARPET_VERSION = 3
+ CACHE_COMMONMARK_VERSION_START = 10
+ CACHE_COMMONMARK_VERSION = 11
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
@@ -49,12 +51,14 @@ module CacheMarkdownField
# Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project)
- group = self.group if self.respond_to?(:group)
+ group = self.group if self.respond_to?(:group)
context = cached_markdown_fields[field].merge(project: project, group: group)
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
+ context[:markdown_engine] = markdown_engine
+
context
end
@@ -69,7 +73,7 @@ module CacheMarkdownField
Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
]
end.to_h
- updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
+ updates['cached_markdown_version'] = latest_cached_markdown_version
updates.each {|html_field, data| write_attribute(html_field, data) }
end
@@ -90,7 +94,7 @@ module CacheMarkdownField
markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_field) || false
- CacheMarkdownField::CACHE_VERSION == cached_markdown_version &&
+ latest_cached_markdown_version == cached_markdown_version &&
(html_changed || markdown_changed == html_changed)
end
@@ -109,6 +113,24 @@ module CacheMarkdownField
__send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend
end
+ def latest_cached_markdown_version
+ return CacheMarkdownField::CACHE_REDCARPET_VERSION unless cached_markdown_version
+
+ if cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
+ CacheMarkdownField::CACHE_REDCARPET_VERSION
+ else
+ CacheMarkdownField::CACHE_COMMONMARK_VERSION
+ end
+ end
+
+ def markdown_engine
+ if latest_cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
+ :redcarpet
+ else
+ :common_mark
+ end
+ end
+
included do
cattr_reader :cached_markdown_fields do
FieldData.new
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index cbec735c2dd..96a43006642 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -23,9 +23,12 @@ class InternalId < ActiveRecord::Base
#
# 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.
- 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!
- self.last_value = (last_value || 0) + 1
+ self.last_value = [(last_value || 0) + 1, (maximum_iid || 0) + 1].max
save!
last_value
end
@@ -89,7 +92,16 @@ class InternalId < ActiveRecord::Base
# and increment its last value
#
# 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
@@ -115,11 +127,15 @@ class InternalId < ActiveRecord::Base
InternalId.create!(
**scope,
usage: usage_value,
- last_value: init.call(subject) || 0
+ last_value: maximum_iid
)
end
rescue ActiveRecord::RecordNotUnique
lookup
end
+
+ def maximum_iid
+ @maximum_iid ||= init.call(subject) || 0
+ end
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index c1ffe6512ea..702bfc77803 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -49,6 +49,7 @@ class Issue < ActiveRecord::Base
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
+ scope :due_tomorrow, -> { where(due_date: Date.tomorrow) }
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index b3ffad00a07..2c3580bbdc6 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -83,14 +83,14 @@ class NotificationRecipient
def has_access?
DeclarativePolicy.subject_scope do
- return false unless user.can?(:receive_notifications)
- return true if @skip_read_ability
+ break false unless user.can?(:receive_notifications)
+ break true if @skip_read_ability
- return false if @target && !user.can?(:read_cross_project)
- return false if @project && !user.can?(:read_project, @project)
+ break false if @target && !user.can?(:read_cross_project)
+ break false if @project && !user.can?(:read_project, @project)
- return true unless read_ability
- return true unless DeclarativePolicy.has_policy?(@target)
+ break true unless read_ability
+ break true unless DeclarativePolicy.has_policy?(@target)
user.can?(read_ability, @target)
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index f6d9b0215fc..9195408551f 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -47,7 +47,8 @@ class NotificationSetting < ActiveRecord::Base
].freeze
EXCLUDED_WATCHER_EVENTS = [
- :push_to_merge_request
+ :push_to_merge_request,
+ :issue_due
].push(*EXCLUDED_PARTICIPATING_EVENTS).freeze
def self.find_or_create_for(source)
diff --git a/app/models/project.rb b/app/models/project.rb
index ffd78d3ab70..38139a9b137 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1637,7 +1637,7 @@ class Project < ActiveRecord::Base
def container_registry_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- return variables unless Gitlab.config.registry.enabled
+ break variables unless Gitlab.config.registry.enabled
variables.append(key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index fd1afafe4df..5bdaa7f0720 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -331,6 +331,7 @@ class Repository
return unless empty?
expire_method_caches(%i(has_visible_content?))
+ raw_repository.expire_has_local_branches_cache
end
def lookup_cache
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index e09b445636f..c289b44f885 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -4,6 +4,9 @@ module Ci
class RegisterJobService
attr_reader :runner
+ JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30].freeze
+ JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET = 5.freeze
+
Result = Struct.new(:build, :valid?)
def initialize(runner)
@@ -30,7 +33,7 @@ module Ci
end
end
- builds.find do |build|
+ builds.auto_include(false).find do |build|
next unless runner.can_pick?(build)
begin
@@ -41,7 +44,7 @@ module Ci
build.run!
register_success(build)
- return Result.new(build, true)
+ return Result.new(build, true) # rubocop:disable Cop/AvoidReturnFromBlocks
rescue Ci::Build::MissingDependenciesError
build.drop!(:missing_dependency_failure)
end
@@ -104,10 +107,22 @@ module Ci
end
def register_success(job)
- job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at)
+ labels = { shared_runner: runner.shared?,
+ jobs_running_for_project: jobs_running_for_project(job) }
+
+ job_queue_duration_seconds.observe(labels, Time.now - job.queued_at) unless job.queued_at.nil?
attempt_counter.increment
end
+ def jobs_running_for_project(job)
+ return '+Inf' unless runner.shared?
+
+ # excluding currently started job
+ running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.shared)
+ .limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1
+ running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
+ end
+
def failed_attempt_counter
@failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job")
end
@@ -117,7 +132,7 @@ module Ci
end
def job_queue_duration_seconds
- @job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time')
+ @job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time', {}, JOB_QUEUE_DURATION_SECONDS_BUCKETS)
end
end
end
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
index 15ab2d54404..84944e95542 100644
--- a/app/services/clusters/gcp/finalize_creation_service.rb
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -13,7 +13,7 @@ module Clusters
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
rescue ActiveRecord::RecordInvalid => e
- provider.make_errored!("Failed to configure GKE Cluster: #{e.message}")
+ provider.make_errored!("Failed to configure Google Kubernetes Engine Cluster: #{e.message}")
end
private
diff --git a/app/services/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb
index f994aacd086..7cc4324677e 100644
--- a/app/services/clusters/gcp/verify_provision_status_service.rb
+++ b/app/services/clusters/gcp/verify_provision_status_service.rb
@@ -17,7 +17,7 @@ module Clusters
when 'DONE'
finalize_creation
else
- return provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
+ provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
end
end
end
diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb
index 88dfb7a4a90..7e5a77fb056 100644
--- a/app/services/create_deployment_service.rb
+++ b/app/services/create_deployment_service.rb
@@ -19,8 +19,8 @@ class CreateDeploymentService
environment.fire_state_event(action)
- return unless environment.save
- return if environment.stopped?
+ break unless environment.save
+ break if environment.stopped?
deploy.tap(&:update_merge_request_metrics!)
end
diff --git a/app/services/import_export_clean_up_service.rb b/app/services/import_export_clean_up_service.rb
index 6442406d77e..74088b970c9 100644
--- a/app/services/import_export_clean_up_service.rb
+++ b/app/services/import_export_clean_up_service.rb
@@ -10,7 +10,7 @@ class ImportExportCleanUpService
def execute
Gitlab::Metrics.measure(:import_export_clean_up) do
- return unless File.directory?(path)
+ next unless File.directory?(path)
clean_up_export_files
end
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index b82d9c64296..83e59a649b6 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -203,10 +203,11 @@ module NotificationRecipientService
attr_reader :action
attr_reader :previous_assignee
attr_reader :skip_current_user
- def initialize(target, current_user, action:, previous_assignee: nil, skip_current_user: true)
+ def initialize(target, current_user, action:, custom_action: nil, previous_assignee: nil, skip_current_user: true)
@target = target
@current_user = current_user
@action = action
+ @custom_action = custom_action
@previous_assignee = previous_assignee
@skip_current_user = skip_current_user
end
@@ -236,7 +237,13 @@ module NotificationRecipientService
add_mentions(current_user, target: target)
# Add the assigned users, if any
- assignees = custom_action == :new_issue ? target.assignees : target.assignee
+ assignees = case custom_action
+ when :new_issue
+ target.assignees
+ else
+ target.assignee
+ end
+
# We use the `:participating` notification level in order to match existing legacy behavior as captured
# in existing specs (notification_service_spec.rb ~ line 507)
add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index f94c76cf3ac..274161df946 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -373,6 +373,20 @@ class NotificationService
end
end
+ def issue_due(issue)
+ recipients = NotificationRecipientService.build_recipients(
+ issue,
+ issue.author,
+ action: 'due',
+ custom_action: :issue_due,
+ skip_current_user: false
+ )
+
+ recipients.each do |recipient|
+ mailer.send(:issue_due_email, recipient.user.id, issue.id, recipient.reason).deliver_later
+ end
+ end
+
protected
def new_resource_email(target, method)
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index aa14206db3b..44e869851ca 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -137,7 +137,7 @@ module Projects
return true unless Gitlab.config.registry.enabled
ContainerRepository.build_root_repository(project).tap do |repository|
- return repository.has_tags? ? repository.delete_tags! : true
+ break repository.has_tags? ? repository.delete_tags! : true
end
end
diff --git a/app/services/repository_archive_clean_up_service.rb b/app/services/repository_archive_clean_up_service.rb
index aa84d36a206..9a88459b511 100644
--- a/app/services/repository_archive_clean_up_service.rb
+++ b/app/services/repository_archive_clean_up_service.rb
@@ -10,7 +10,7 @@ class RepositoryArchiveCleanUpService
def execute
Gitlab::Metrics.measure(:repository_archive_clean_up) do
- return unless File.directory?(path)
+ next unless File.directory?(path)
clean_up_old_archives
clean_up_empty_directories
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 00bf5434b7f..958ef065012 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -159,7 +159,7 @@ module SystemNoteService
body = if noteable.time_estimate == 0
"removed time estimate"
else
- "changed time estimate to #{parsed_time}"
+ "changed time estimate to #{parsed_time},"
end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
diff --git a/app/services/test_hooks/base_service.rb b/app/services/test_hooks/base_service.rb
index e9aefb1fb75..aadc1ea644b 100644
--- a/app/services/test_hooks/base_service.rb
+++ b/app/services/test_hooks/base_service.rb
@@ -19,7 +19,7 @@ module TestHooks
error_message = catch(:validation_error) do
sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend
- return hook.execute(sample_data, trigger_key)
+ return hook.execute(sample_data, trigger_key) # rubocop:disable Cop/AvoidReturnFromBlocks
end
error(error_message)
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 05c41082882..bbf0e0fb95c 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -126,6 +126,7 @@
GitLab
%span.pull-right
= Gitlab::VERSION
+ = "(#{Gitlab::REVISION})"
%p
GitLab Shell
%span.pull-right
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index 70ec6bc6257..d7b6fb9a4a1 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.atom.builder
@@ -1,5 +1,5 @@
xml.title "#{current_user.name} issues"
-xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
+xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index bb472b4c900..4bf04dadf01 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -2,12 +2,12 @@
- page_title _("Issues")
- @breadcrumb_link = issues_dashboard_path(assignee_id: current_user.id)
= content_for :meta_tags do
- = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues")
+ = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues")
.top-area
= render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set
.nav-controls
- = link_to params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do
+ = link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do
= icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 8680ec2e298..646e89e9bd1 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -7,7 +7,7 @@
- unless expanded
- diff_data = { lines_path: project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion) }
-.diff-file.file-holder{ class: diff_file_class, data: diff_data }
+.diff-file.file-holder.js-lazy-load-discussion{ class: diff_file_class, data: diff_data }
.js-file-title.file-title.file-title-flex-parent
.file-header-content
= render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false
@@ -28,8 +28,11 @@
%tr.line_holder.line-holder-placeholder
%td.old_line.diff-line-num
%td.new_line.diff-line-num
- %td.line_content
+ %td.line_content.js-success-lazy-load
.js-code-placeholder
+ %td.js-error-lazy-load-diff.hidden.diff-loading-error-block
+ - button = button_tag(_("Try again"), class: "btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button")
+ = _("Unable to load the diff. %{button_try_again}").html_safe % { button_try_again: button}
= render "discussions/diff_discussion", discussions: [discussion], expanded: true
- else
- partial = (diff_file.new_file? || diff_file.deleted_file?) ? 'single_image_diff' : 'replaced_image_diff'
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index a239ea8caf0..2a385b661e5 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.atom.builder
@@ -1,5 +1,5 @@
xml.title "#{@group.name} issues"
-xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
+xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: issues_group_url, rel: "alternate", type: "text/html"
xml.id issues_group_url
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 36df03302e8..bbfbea4ac7a 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -1,6 +1,6 @@
- page_title "Issues"
= content_for :meta_tags do
- = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues")
+ = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues")
- if group_issues_count(state: 'all').zero?
= render 'shared/empty_states/issues', project_select_button: true
diff --git a/app/views/notify/issue_due_email.html.haml b/app/views/notify/issue_due_email.html.haml
new file mode 100644
index 00000000000..e81144b8fcb
--- /dev/null
+++ b/app/views/notify/issue_due_email.html.haml
@@ -0,0 +1,12 @@
+%p.details
+ #{link_to @issue.author_name, user_url(@issue.author)}'s issue is due soon.
+
+- if @issue.assignees.any?
+ %p
+ Assignee: #{@issue.assignee_list}
+%p
+ This issue is due on: #{@issue.due_date.to_s(:medium)}
+
+- if @issue.description
+ %div
+ = markdown(@issue.description, pipeline: :email, author: @issue.author)
diff --git a/app/views/notify/issue_due_email.text.erb b/app/views/notify/issue_due_email.text.erb
new file mode 100644
index 00000000000..3c7a57a8a2e
--- /dev/null
+++ b/app/views/notify/issue_due_email.text.erb
@@ -0,0 +1,7 @@
+The following issue is due on <%= @issue.due_date %>:
+
+Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
+Author: <%= @issue.author_name %>
+Assignee: <%= @issue.assignee_list %>
+
+<%= @issue.description %>
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 1bd10018b40..d1eae05c46c 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -20,7 +20,7 @@
- else
%p
Download the Google Authenticator application from App Store or Google Play Store and scan this code.
- More information is available in the #{link_to('documentation', help_page_path('profile/two_factor_authentication'))}.
+ More information is available in the #{link_to('documentation', help_page_path('user/profile/account/two_factor_authentication'))}.
.row.append-bottom-10
.col-md-4
= raw @qr_code
diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml
index 3124443b4e4..b9663bbba15 100644
--- a/app/views/projects/blob/_viewer.html.haml
+++ b/app/views/projects/blob/_viewer.html.haml
@@ -2,13 +2,16 @@
- render_error = viewer.render_error
- rich_type = viewer.type == :rich ? viewer.partial_name : nil
- load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?)
+- external_embed = local_assigns.fetch(:external_embed, false)
-- viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async
+- viewer_url = local_assigns.fetch(:viewer_url) { url_for(safe_params.merge(viewer: viewer.type, format: :json)) } if load_async
.blob-viewer{ data: { type: viewer.type, rich_type: rich_type, url: viewer_url }, class: ('hidden' if hidden) }
- if render_error
= render 'projects/blob/render_error', viewer: viewer
- elsif load_async
= render viewer.loading_partial_path, viewer: viewer
+ - elsif external_embed
+ = render 'projects/blob/viewers/highlight_embed', blob: viewer.blob
- else
- viewer.prepare!
diff --git a/app/views/projects/blob/viewers/_highlight_embed.html.haml b/app/views/projects/blob/viewers/_highlight_embed.html.haml
new file mode 100644
index 00000000000..9bd4ef6ad0b
--- /dev/null
+++ b/app/views/projects/blob/viewers/_highlight_embed.html.haml
@@ -0,0 +1,7 @@
+.file-content.code.js-syntax-highlight
+ .line-numbers
+ - if blob.data.present?
+ - blob.data.each_line.each_with_index do |_, index|
+ %span.diff-line-num= index + 1
+ .blob-content{ data: { blob_id: blob.id } }
+ = highlight(blob.path, blob.data, repository: nil, plain: blob.no_highlighting?)
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 71176acd12d..d0c01f95cb7 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -29,7 +29,7 @@
= s_('Branches|Cant find HEAD commit for this branch')
- if branch.name != @repository.root_ref
- .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
+ .divergence-graph.hidden-xs{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
default_branch: @repository.root_ref,
number_commits_ahead: diverging_count_label(number_commits_ahead) } }
.graph-side
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index ebb7d247125..e004966bdcc 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -8,6 +8,6 @@
%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration')
%p= s_('ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab')
- = link_to s_('ClusterIntegration|Create on GKE'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
+ = link_to s_('ClusterIntegration|Create on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
%p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
= link_to s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 289bfdd69bc..3fd0fa348b3 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -22,7 +22,7 @@
= author_avatar(commit, size: 36)
.commit-detail.flex-list
- .commit-content
+ .commit-content.qa-commit-content
- if view_details && merge_request
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title"
- else
diff --git a/app/views/projects/diffs/_collapsed.html.haml b/app/views/projects/diffs/_collapsed.html.haml
index 8772bd4705f..5762f4d86d7 100644
--- a/app/views/projects/diffs/_collapsed.html.haml
+++ b/app/views/projects/diffs/_collapsed.html.haml
@@ -1,5 +1,5 @@
- diff_file = viewer.diff_file
-- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
+- url = url_for(safe_params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
.nothing-here-block.diff-collapsed{ data: { diff_for_path: url } }
This diff is collapsed.
%a.click-to-expand Click to expand it.
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml
index dd1a836fa20..297b928f020 100644
--- a/app/views/projects/issues/_nav_btns.html.haml
+++ b/app/views/projects/issues/_nav_btns.html.haml
@@ -1,4 +1,4 @@
-= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
+= link_to safe_params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss')
- if @can_bulk_update
= button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index 4029926f373..6330245954e 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/index.atom.builder
@@ -1,5 +1,5 @@
xml.title "#{@project.name} issues"
-xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
+xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: project_issues_url(@project), rel: "alternate", type: "text/html"
xml.id project_issues_url(@project)
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index c427a9eedc2..1e7737aeb97 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -5,7 +5,7 @@
- new_issue_email = @project.new_issuable_address(current_user, 'issue')
= content_for :meta_tags do
- = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
+ = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@project.name} issues")
- if project_issues(@project).exists?
%div{ class: (container_class) }
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index 9d5cebdda53..f81db9b4e28 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -3,7 +3,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f|
.hide.alert.alert-danger.mr-compare-errors
- .merge-request-branches.js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
+ .js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
.col-md-6
.panel.panel-default.panel-new-merge-request
.panel-heading
diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml
index 376ac377562..68780cedeb1 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -26,16 +26,16 @@
- else
%ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.commits-tab.active
- = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
+ = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
Commits
%span.badge= @commits.size
- if @pipelines.any?
%li.builds-tab
- = link_to url_for(params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
+ = link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
Pipelines
%span.badge= @pipelines.size
%li.diffs-tab
- = link_to url_for(params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
+ = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
Changes
%span.badge= @merge_request.diff_size
@@ -46,7 +46,7 @@
-# This tab is always loaded via AJAX
- if @pipelines.any?
#pipelines.pipelines.tab-pane
- = render 'projects/merge_requests/pipelines', endpoint: url_for(params.merge(action: 'pipelines', format: :json)), disable_initialization: true
+ = render 'projects/merge_requests/pipelines', endpoint: url_for(safe_params.merge(action: 'pipelines', format: :json)), disable_initialization: true
.mr-loading-status
= spinner
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 877101b05ca..8f2142af2ce 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -1,24 +1,25 @@
- breadcrumb_title "Pipelines"
-- page_title "New Pipeline"
+- page_title = s_("Pipeline|Run Pipeline")
%h3.page-title
- New Pipeline
+ = s_("Pipeline|Run Pipeline")
%hr
= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f|
= form_errors(@pipeline)
.form-group
- = f.label :ref, 'Create for', class: 'control-label'
+ = f.label :ref, s_('Pipeline|Run on'), class: 'control-label'
.col-sm-10
= hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide git-revision-dropdown-toggle',
- filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: "Search branches",
+ filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"),
data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
- .help-block Existing branch name, tag
+ .help-block
+ = s_("Pipeline|Existing branch name, tag")
.form-actions
- = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
- = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-cancel'
+ = f.submit s_('Pipeline|Run pipeline'), class: 'btn btn-success', tabindex: 3
+ = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default pull-right'
-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml
index 98d56a3e5c5..12ccae10260 100644
--- a/app/views/projects/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -7,8 +7,8 @@
- content_for :push_access_levels do
.push_access_levels-container
= dropdown_tag('Select',
- options: { toggle_class: 'js-allowed-to-push wide',
- dropdown_class: 'dropdown-menu-selectable capitalize-header',
+ options: { toggle_class: 'js-allowed-to-push qa-allowed-to-push-select wide',
+ dropdown_class: 'dropdown-menu-selectable qa-allowed-to-push-dropdown capitalize-header',
data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
= render 'projects/protected_branches/shared/create_protected_branch'
diff --git a/app/views/projects/protected_branches/_update_protected_branch.html.haml b/app/views/projects/protected_branches/_update_protected_branch.html.haml
index c61b2951e1e..98363f2018a 100644
--- a/app/views/projects/protected_branches/_update_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_update_protected_branch.html.haml
@@ -6,5 +6,5 @@
%td
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level
= dropdown_tag( (protected_branch.push_access_levels.first.humanize || 'Select') ,
- options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
+ options: { toggle_class: 'js-allowed-to-push qa-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: protected_branch.push_access_levels.first.id }})
diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml
index 300055a4207..d1ed438eb21 100644
--- a/app/views/projects/protected_branches/shared/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml
@@ -1,4 +1,4 @@
-.protected-branches-list.js-protected-branches-list
+.protected-branches-list.js-protected-branches-list.qa-protected-branches-list
- if @protected_branches.empty?
.panel-heading
%h3.panel-title
diff --git a/app/views/projects/protected_branches/shared/_dropdown.html.haml b/app/views/projects/protected_branches/shared/_dropdown.html.haml
index 74435236808..b3d6068039a 100644
--- a/app/views/projects/protected_branches/shared/_dropdown.html.haml
+++ b/app/views/projects/protected_branches/shared/_dropdown.html.haml
@@ -1,8 +1,8 @@
= f.hidden_field(:name)
= dropdown_tag('Select branch or create wildcard',
- options: { toggle_class: 'js-protected-branch-select js-filter-submit wide git-revision-dropdown-toggle',
- filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: "Search protected branches",
+ options: { toggle_class: 'js-protected-branch-select js-filter-submit wide git-revision-dropdown-toggle qa-protected-branch-select',
+ filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown qa-protected-branch-dropdown", placeholder: "Search protected branches",
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_branch_name],
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index 55d87c35a80..fd5c1aa342a 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -4,7 +4,7 @@
.settings-header
%h4
Protected Branches
- %button.btn.js-settings-toggle{ type: 'button' }
+ %button.btn.js-settings-toggle.qa-expand-protected-branches{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
index 10b81e42572..f5b21f0e887 100644
--- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
@@ -2,7 +2,7 @@
%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) } }
%td
- %span.ref-name= protected_branch.name
+ %span.ref-name.qa-protected-branch-name= protected_branch.name
- if @project.root_ref?(protected_branch.name)
%span.label.label-info.prepend-left-5 default
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
index 934d65e8b42..e9ac192f5f7 100644
--- a/app/views/shared/_auto_devops_callout.html.haml
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -1,14 +1,14 @@
-.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
+.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20.prepend-top-10{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
.banner-graphic
= custom_icon('icon_autodevops')
- .prepend-top-10.prepend-left-10.append-bottom-10
- %h5= s_('AutoDevOps|Auto DevOps (Beta)')
+ .banner-body.prepend-left-10.append-bottom-10
+ %h5.banner-title= s_('AutoDevOps|Auto DevOps (Beta)')
%p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
%p
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
- .prepend-top-10
+ .banner-buttons
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout'
%button.btn-transparent.banner-close.close.js-close-callout{ type: 'button',
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index 4c8c92d722a..f1c39b9e923 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -8,8 +8,8 @@
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
.dropdown
- = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" }
- .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
+ = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown qa-branches-select" }
+ .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging.qa-branches-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
.dropdown-page-one
= dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags")
diff --git a/app/views/shared/milestones/_deprecation_message.html.haml b/app/views/shared/milestones/_deprecation_message.html.haml
new file mode 100644
index 00000000000..4a8f90937ea
--- /dev/null
+++ b/app/views/shared/milestones/_deprecation_message.html.haml
@@ -0,0 +1,14 @@
+.banner-callout.compact.milestone-deprecation-message.js-milestone-deprecation-message.prepend-top-20
+ .banner-graphic= image_tag 'illustrations/milestone_removing-page.svg'
+ .banner-body.prepend-left-10.append-right-10
+ %h5.banner-title.prepend-top-0= _('This page will be removed in a future release.')
+ %p.milestone-banner-text= _('Use group milestones to manage issues from multiple projects in the same milestone.')
+ = button_tag _('Promote these project milestones into a group milestone.'), class: 'btn btn-link js-popover-link text-align-left milestone-banner-link'
+ .milestone-banner-buttons.prepend-top-20= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-default', target: '_blank'
+
+ %template.js-milestone-deprecation-message-template
+ .milestone-popover-body
+ %ol.milestone-popover-instructions-list.append-bottom-0
+ %li= _('Click any <strong>project name</strong> in the project list below to navigate to the project milestone.').html_safe
+ %li= _('Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone.').html_safe
+ .milestone-popover-footer= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-link prepend-left-0', target: '_blank'
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index f302299eb24..797ff034bb2 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -1,7 +1,8 @@
-- page_title @milestone.title
+- page_title milestone.title
- @breadcrumb_link = dashboard_milestone_path(milestone.safe_title, title: milestone.title)
- group = local_assigns[:group]
+- is_dynamic_milestone = milestone.legacy_group_milestone? || milestone.dashboard_milestone?
.detail-page-header
%a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
@@ -31,21 +32,23 @@
- else
= link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
+= render 'shared/milestones/deprecation_message' if is_dynamic_milestone
+
.detail-page-description.milestone-detail
%h2.title
= markdown_field(milestone, :title)
- - if @milestone.group_milestone? && @milestone.description.present?
+ - if milestone.group_milestone? && milestone.description.present?
%div
.description
.wiki
- = markdown_field(@milestone, :description)
+ = markdown_field(milestone, :description)
- if milestone.complete?(current_user) && milestone.active?
.alert.alert-success.prepend-top-default
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg}
-- if @milestone.legacy_group_milestone? || @milestone.dashboard_milestone?
+- if is_dynamic_milestone
.table-holder
%table.table
%thead
@@ -68,7 +71,7 @@
Open
%td
= ms.expires_at
-- elsif @milestone.group_milestone?
+- elsif milestone.group_milestone?
%br
View
= link_to 'Issues', issues_group_path(@group, milestone_title: milestone.title)
diff --git a/app/views/shared/snippets/_embed.html.haml b/app/views/shared/snippets/_embed.html.haml
new file mode 100644
index 00000000000..2d93e51a2d9
--- /dev/null
+++ b/app/views/shared/snippets/_embed.html.haml
@@ -0,0 +1,24 @@
+- blob = @snippet.blob
+.gitlab-embed-snippets
+ .js-file-title.file-title-flex-parent
+ .file-header-content
+ = external_snippet_icon('doc_text')
+
+ %strong.file-title-name
+ %a.gitlab-embedded-snippets-title{ href: url_for(only_path: false, overwrite_params: nil) }
+ = blob.name
+
+ %small
+ = number_to_human_size(blob.raw_size)
+ %a.gitlab-logo{ href: url_for(only_path: false, overwrite_params: nil), title: 'view on gitlab' }
+ on &nbsp;
+ %span.logo-text
+ GitLab
+
+ .file-actions.hidden-xs
+ .btn-group{ role: "group" }<
+ = embedded_snippet_raw_button
+
+ = embedded_snippet_download_button
+ %article.file-holder.snippet-file-content
+ = render 'projects/blob/viewer', viewer: @snippet.blob.simple_viewer, load_async: false, external_embed: true
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 12df79a28c7..836230ae8ee 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -19,11 +19,32 @@
%h2.snippet-title.prepend-top-0.append-bottom-0
= markdown_field(@snippet, :title)
- - if @snippet.updated_at != @snippet.created_at
- = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true)
- if @snippet.description.present?
.description
.wiki
= markdown_field(@snippet, :description)
%textarea.hidden.js-task-list-field
= @snippet.description
+
+ - if @snippet.updated_at != @snippet.created_at
+ = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true)
+
+ - if public_snippet?
+ .embed-snippet
+ .input-group
+ .input-group-btn
+ %button.btn.embed-toggle{ 'data-toggle': 'dropdown', type: 'button' }
+ %span.js-embed-action= _("Embed")
+ = sprite_icon('angle-down', size: 12)
+ %ul.dropdown-menu.dropdown-menu-selectable.embed-toggle-list
+ %li
+ %button.js-embed-btn.btn.btn-transparent.is-active{ type: 'button' }
+ %strong.embed-toggle-list-item= _("Embed")
+ %li
+ %button.js-share-btn.btn.btn-transparent{ type: 'button' }
+ %strong.embed-toggle-list-item= _("Share")
+ %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed }
+ .input-group-btn
+ %button.js-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '#snippet-url-area' }
+ = sprite_icon('duplicate', size: 16)
+ .clearfix
diff --git a/app/views/shared/snippets/show.js.haml b/app/views/shared/snippets/show.js.haml
new file mode 100644
index 00000000000..a9af732bbb5
--- /dev/null
+++ b/app/views/shared/snippets/show.js.haml
@@ -0,0 +1,2 @@
+document.write('#{escape_javascript(stylesheet_link_tag "#{stylesheet_url 'snippets'}")}');
+document.write('#{escape_javascript(render 'shared/snippets/embed')}');
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 9a11cdb121e..9aea3bad27b 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -18,6 +18,7 @@
- cronjob:stuck_import_jobs
- cronjob:stuck_merge_jobs
- cronjob:trending_projects
+- cronjob:issue_due_scheduler
- gcp_cluster:cluster_install_app
- gcp_cluster:cluster_provision
@@ -39,6 +40,8 @@
- github_importer:github_import_stage_import_pull_requests
- github_importer:github_import_stage_import_repository
+- mail_scheduler:mail_scheduler_issue_due
+
- object_storage_upload
- object_storage:object_storage_background_move
- object_storage:object_storage_migrate_uploads
diff --git a/app/workers/concerns/mail_scheduler_queue.rb b/app/workers/concerns/mail_scheduler_queue.rb
new file mode 100644
index 00000000000..9df55ad9522
--- /dev/null
+++ b/app/workers/concerns/mail_scheduler_queue.rb
@@ -0,0 +1,7 @@
+module MailSchedulerQueue
+ extend ActiveSupport::Concern
+
+ included do
+ queue_namespace :mail_scheduler
+ end
+end
diff --git a/app/workers/issue_due_scheduler_worker.rb b/app/workers/issue_due_scheduler_worker.rb
new file mode 100644
index 00000000000..16ab5d069e0
--- /dev/null
+++ b/app/workers/issue_due_scheduler_worker.rb
@@ -0,0 +1,10 @@
+class IssueDueSchedulerWorker
+ include ApplicationWorker
+ include CronjobQueue
+
+ def perform
+ project_ids = Issue.opened.due_tomorrow.group(:project_id).pluck(:project_id).map { |id| [id] }
+
+ MailScheduler::IssueDueWorker.bulk_perform_async(project_ids)
+ end
+end
diff --git a/app/workers/mail_scheduler/issue_due_worker.rb b/app/workers/mail_scheduler/issue_due_worker.rb
new file mode 100644
index 00000000000..b06079d68ca
--- /dev/null
+++ b/app/workers/mail_scheduler/issue_due_worker.rb
@@ -0,0 +1,14 @@
+module MailScheduler
+ class IssueDueWorker
+ include ApplicationWorker
+ include MailSchedulerQueue
+
+ def perform(project_id)
+ notification_service = NotificationService.new
+
+ Issue.opened.due_tomorrow.in_projects(project_id).preload(:project).find_each do |issue|
+ notification_service.issue_due(issue)
+ end
+ end
+ end
+end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 3909dbf7d7f..f88b3fdbfb1 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -33,7 +33,7 @@ class PostReceive
unless @user
log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
- return false
+ return false # rubocop:disable Cop/AvoidReturnFromBlocks
end
if Gitlab::Git.tag_ref?(ref)
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index fb26fa4c515..7ebf69bdc39 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -38,7 +38,7 @@ class StuckCiJobsWorker
def drop_stuck(status, timeout)
search(status, timeout) do |build|
- return unless build.stuck?
+ break unless build.stuck?
drop_build :stuck, build, status, timeout
end