Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorKamil Trzciński <ayufan@ayufan.eu>2018-09-07 00:06:55 +0300
committerKamil Trzciński <ayufan@ayufan.eu>2018-09-07 00:06:55 +0300
commit915306ec50c50b3b1b0793150a7cb4f31fbbee75 (patch)
treec9f6a045ee02003ca9b715783692cb1d623f619d /app
parentc353773787407158b6319b6944dee57a6e3522b9 (diff)
parent7aa6766cd15036dcfb5391b3fd7bffb9069ffbcf (diff)
Merge branch 'master' into 'alerts-for-built-in-metrics'
# Conflicts: # db/schema.rb
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/auth_buttons/auth0_64.pngbin0 -> 1815 bytes
-rw-r--r--app/assets/images/auth_buttons/azure_64.pngbin695 -> 199 bytes
-rw-r--r--app/assets/images/auth_buttons/bitbucket_64.pngbin2161 -> 1299 bytes
-rw-r--r--app/assets/images/auth_buttons/google_64.pngbin4366 -> 1625 bytes
-rw-r--r--app/assets/images/auth_buttons/jwt_64.pngbin0 -> 2457 bytes
-rw-r--r--app/assets/images/auth_buttons/shibboleth_64.pngbin0 -> 2993 bytes
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js39
-rw-r--r--app/assets/javascripts/clusters/clusters_index.js4
-rw-r--r--app/assets/javascripts/diffs/components/changed_files_dropdown.vue2
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_comment_row.vue7
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js1
-rw-r--r--app/assets/javascripts/dismissable_callout.js (renamed from app/assets/javascripts/clusters/components/gcp_signup_offer.js)4
-rw-r--r--app/assets/javascripts/fly_out_nav.js4
-rw-r--r--app/assets/javascripts/groups/components/groups.vue2
-rw-r--r--app/assets/javascripts/ide/stores/modules/branches/mutations.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/mutations.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutations.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js1
-rw-r--r--app/assets/javascripts/jobs/store/mutations.js2
-rw-r--r--app/assets/javascripts/notes.js2
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue7
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/index.js5
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js62
-rw-r--r--app/assets/javascripts/pages/instance_statistics/cohorts/index.js3
-rw-r--r--app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js13
-rw-r--r--app/assets/javascripts/pages/projects/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/project.js57
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js5
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue2
-rw-r--r--app/assets/javascripts/read_more.js41
-rw-r--r--app/assets/javascripts/reports/store/mutations.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue42
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js2
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/awards.scss4
-rw-r--r--app/assets/stylesheets/framework/buttons.scss8
-rw-r--r--app/assets/stylesheets/framework/filters.scss4
-rw-r--r--app/assets/stylesheets/framework/mobile.scss13
-rw-r--r--app/assets/stylesheets/framework/read_more.scss13
-rw-r--r--app/assets/stylesheets/framework/toggle.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss3
-rw-r--r--app/assets/stylesheets/pages/admin.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss4
-rw-r--r--app/assets/stylesheets/pages/login.scss29
-rw-r--r--app/assets/stylesheets/pages/notes.scss14
-rw-r--r--app/assets/stylesheets/pages/projects.scss235
-rw-r--r--app/assets/stylesheets/pages/settings.scss2
-rw-r--r--app/controllers/application_controller.rb1
-rw-r--r--app/controllers/concerns/issuable_collections.rb12
-rw-r--r--app/controllers/concerns/send_file_upload.rb16
-rw-r--r--app/controllers/concerns/uploads_actions.rb2
-rw-r--r--app/controllers/groups/labels_controller.rb6
-rw-r--r--app/controllers/instance_statistics/cohorts_controller.rb6
-rw-r--r--app/controllers/projects/application_controller.rb3
-rw-r--r--app/controllers/projects/clusters_controller.rb3
-rw-r--r--app/controllers/projects/hooks_controller.rb1
-rw-r--r--app/controllers/projects/refs_controller.rb65
-rw-r--r--app/finders/template_finder.rb37
-rw-r--r--app/helpers/blob_helper.rb29
-rw-r--r--app/helpers/button_helper.rb9
-rw-r--r--app/helpers/clusters_helper.rb4
-rw-r--r--app/helpers/cookies_helper.rb9
-rw-r--r--app/helpers/events_helper.rb16
-rw-r--r--app/helpers/icons_helper.rb4
-rw-r--r--app/helpers/markup_helper.rb16
-rw-r--r--app/helpers/projects_helper.rb8
-rw-r--r--app/helpers/user_callouts_helper.rb5
-rw-r--r--app/helpers/visibility_level_helper.rb2
-rw-r--r--app/helpers/wiki_helper.rb6
-rw-r--r--app/mailers/emails/auto_devops.rb24
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/mailers/previews/notify_preview.rb4
-rw-r--r--app/models/ci/job_artifact.rb22
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/models/clusters/applications/helm.rb3
-rw-r--r--app/models/clusters/applications/ingress.rb1
-rw-r--r--app/models/clusters/applications/jupyter.rb1
-rw-r--r--app/models/clusters/applications/prometheus.rb3
-rw-r--r--app/models/clusters/applications/runner.rb1
-rw-r--r--app/models/clusters/cluster.rb1
-rw-r--r--app/models/clusters/platforms/kubernetes.rb27
-rw-r--r--app/models/commit.rb1
-rw-r--r--app/models/concerns/avatarable.rb2
-rw-r--r--app/models/concerns/case_sensitivity.rb45
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/concerns/protected_ref.rb10
-rw-r--r--app/models/concerns/triggerable_hooks.rb6
-rw-r--r--app/models/hooks/active_hook_filter.rb14
-rw-r--r--app/models/hooks/web_hook.rb1
-rw-r--r--app/models/label_link.rb2
-rw-r--r--app/models/project.rb3
-rw-r--r--app/models/project_services/kubernetes_service.rb26
-rw-r--r--app/models/protected_ref_matcher.rb56
-rw-r--r--app/models/ref_matcher.rb46
-rw-r--r--app/models/repository.rb7
-rw-r--r--app/models/user.rb5
-rw-r--r--app/models/user_callout.rb3
-rw-r--r--app/policies/issuable_policy.rb1
-rw-r--r--app/policies/issue_policy.rb4
-rw-r--r--app/policies/project_policy.rb1
-rw-r--r--app/presenters/project_presenter.rb158
-rw-r--r--app/serializers/build_details_entity.rb8
-rw-r--r--app/services/files/base_service.rb4
-rw-r--r--app/services/issuable_base_service.rb13
-rw-r--r--app/services/issues/reopen_service.rb2
-rw-r--r--app/services/merge_requests/build_service.rb20
-rw-r--r--app/services/merge_requests/create_from_issue_service.rb5
-rw-r--r--app/services/notification_service.rb6
-rw-r--r--app/services/preview_markdown_service.rb6
-rw-r--r--app/services/projects/auto_devops/disable_service.rb39
-rw-r--r--app/services/wikis/create_attachment_service.rb71
-rw-r--r--app/uploaders/file_uploader.rb10
-rw-r--r--app/uploaders/job_artifact_uploader.rb17
-rw-r--r--app/uploaders/uploader_helper.rb27
-rw-r--r--app/validators/branch_filter_validator.rb35
-rw-r--r--app/views/admin/application_settings/_usage.html.haml31
-rw-r--r--app/views/admin/application_settings/show.html.haml8
-rw-r--r--app/views/devise/sessions/_new_base.html.haml4
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml27
-rw-r--r--app/views/devise/shared/_signup_box.html.haml17
-rw-r--r--app/views/discussions/_diff_discussion.html.haml3
-rw-r--r--app/views/discussions/_parallel_diff_discussion.html.haml12
-rw-r--r--app/views/events/_event_push.atom.haml2
-rw-r--r--app/views/groups/labels/index.html.haml26
-rw-r--r--app/views/instance_statistics/cohorts/index.html.haml16
-rw-r--r--app/views/instance_statistics/conversational_development_index/_disabled.html.haml17
-rw-r--r--app/views/instance_statistics/conversational_development_index/index.html.haml7
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/fullscreen.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml21
-rw-r--r--app/views/layouts/nav/sidebar/_instance_statistics.html.haml23
-rw-r--r--app/views/notify/_failed_builds.html.haml32
-rw-r--r--app/views/notify/autodevops_disabled_email.html.haml49
-rw-r--r--app/views/notify/autodevops_disabled_email.text.erb20
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml35
-rw-r--r--app/views/profiles/show.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml83
-rw-r--r--app/views/projects/_stat_anchor_list.html.haml2
-rw-r--r--app/views/projects/_wiki.html.haml2
-rw-r--r--app/views/projects/blob/_blob.html.haml1
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/blob/preview.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_markup.html.haml6
-rw-r--r--app/views/projects/buttons/_fork.html.haml30
-rw-r--r--app/views/projects/buttons/_star.html.haml30
-rw-r--r--app/views/projects/clusters/_banner.html.haml23
-rw-r--r--app/views/projects/clusters/_integration_form.html.haml20
-rw-r--r--app/views/projects/clusters/show.html.haml3
-rw-r--r--app/views/projects/clusters/user/_form.html.haml9
-rw-r--r--app/views/projects/clusters/user/_show.html.haml9
-rw-r--r--app/views/projects/default_branch/_show.html.haml21
-rw-r--r--app/views/projects/edit.html.haml5
-rw-r--r--app/views/projects/empty.html.haml18
-rw-r--r--app/views/projects/issues/new.html.haml3
-rw-r--r--app/views/projects/issues/show.html.haml4
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/new.html.haml3
-rw-r--r--app/views/projects/settings/repository/show.html.haml1
-rw-r--r--app/views/projects/show.html.haml9
-rw-r--r--app/views/projects/tree/_tree_commit_column.html.haml2
-rw-r--r--app/views/projects/wikis/_form.html.haml6
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml5
-rw-r--r--app/views/projects/wikis/show.html.haml2
-rw-r--r--app/views/search/results/_snippet_blob.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml6
-rw-r--r--app/views/shared/_mobile_clone_panel.html.haml13
-rw-r--r--app/views/shared/groups/_empty_state.html.haml2
-rw-r--r--app/views/shared/issuable/_close_reopen_button.html.haml16
-rw-r--r--app/views/shared/web_hooks/_form.html.haml1
-rw-r--r--app/views/users/calendar_activities.html.haml2
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/auto_devops/disable_worker.rb35
-rw-r--r--app/workers/concerns/auto_devops_queue.rb9
176 files changed, 1607 insertions, 807 deletions
diff --git a/app/assets/images/auth_buttons/auth0_64.png b/app/assets/images/auth_buttons/auth0_64.png
new file mode 100644
index 00000000000..5ad59659380
--- /dev/null
+++ b/app/assets/images/auth_buttons/auth0_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/azure_64.png b/app/assets/images/auth_buttons/azure_64.png
index 85de7793440..168a9c81395 100644
--- a/app/assets/images/auth_buttons/azure_64.png
+++ b/app/assets/images/auth_buttons/azure_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/bitbucket_64.png b/app/assets/images/auth_buttons/bitbucket_64.png
index b3d022a5a70..0edf7f52a11 100644
--- a/app/assets/images/auth_buttons/bitbucket_64.png
+++ b/app/assets/images/auth_buttons/bitbucket_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/google_64.png b/app/assets/images/auth_buttons/google_64.png
index 720824230a5..389c1cd54ca 100644
--- a/app/assets/images/auth_buttons/google_64.png
+++ b/app/assets/images/auth_buttons/google_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/jwt_64.png b/app/assets/images/auth_buttons/jwt_64.png
new file mode 100644
index 00000000000..ca97ae47002
--- /dev/null
+++ b/app/assets/images/auth_buttons/jwt_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/shibboleth_64.png b/app/assets/images/auth_buttons/shibboleth_64.png
new file mode 100644
index 00000000000..d4c752f9400
--- /dev/null
+++ b/app/assets/images/auth_buttons/shibboleth_64.png
Binary files differ
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 0fdf0c7a389..ebf76af5966 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -1,16 +1,12 @@
import Visibility from 'visibilityjs';
import Vue from 'vue';
+import initDismissableCallout from '~/dismissable_callout';
import { s__, sprintf } from '../locale';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
-import {
- APPLICATION_STATUS,
- REQUEST_LOADING,
- REQUEST_SUCCESS,
- REQUEST_FAILURE,
-} from './constants';
+import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import applications from './components/applications.vue';
@@ -66,6 +62,7 @@ export default class Clusters {
this.showTokenButton = document.querySelector('.js-show-cluster-token');
this.tokenField = document.querySelector('.js-cluster-token');
+ initDismissableCallout('.js-cluster-security-warning');
initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications();
@@ -129,7 +126,8 @@ export default class Clusters {
if (!Visibility.hidden()) {
this.poll.makeRequest();
} else {
- this.service.fetchData()
+ this.service
+ .fetchData()
.then(data => this.handleSuccess(data))
.catch(() => Clusters.handleError());
}
@@ -177,15 +175,21 @@ export default class Clusters {
checkForNewInstalls(prevApplicationMap, newApplicationMap) {
const appTitles = Object.keys(newApplicationMap)
- .filter(appId => newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
- prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
- prevApplicationMap[appId].status !== null)
+ .filter(
+ appId =>
+ newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
+ prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
+ prevApplicationMap[appId].status !== null,
+ )
.map(appId => newApplicationMap[appId].title);
if (appTitles.length > 0) {
- const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'), {
- appList: appTitles.join(', '),
- });
+ const text = sprintf(
+ s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'),
+ {
+ appList: appTitles.join(', '),
+ },
+ );
Flash(text, 'notice', this.successApplicationContainer);
}
}
@@ -218,13 +222,18 @@ export default class Clusters {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
this.store.updateAppProperty(appId, 'requestReason', null);
- this.service.installApplication(appId, data.params)
+ this.service
+ .installApplication(appId, data.params)
.then(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
})
.catch(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
- this.store.updateAppProperty(appId, 'requestReason', s__('ClusterIntegration|Request to begin installing failed'));
+ this.store.updateAppProperty(
+ appId,
+ 'requestReason',
+ s__('ClusterIntegration|Request to begin installing failed'),
+ );
});
}
diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js
index 1e5c733d151..789c8360124 100644
--- a/app/assets/javascripts/clusters/clusters_index.js
+++ b/app/assets/javascripts/clusters/clusters_index.js
@@ -1,14 +1,14 @@
import createFlash from '~/flash';
import { __ } from '~/locale';
import setupToggleButtons from '~/toggle_buttons';
-import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+import initDismissableCallout from '~/dismissable_callout';
import ClustersService from './services/clusters_service';
export default () => {
const clusterList = document.querySelector('.js-clusters-list');
- gcpSignupOffer();
+ initDismissableCallout('.gcp-signup-offer');
// The empty state won't have a clusterList
if (clusterList) {
diff --git a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
index 045688a32bf..0ec6b8b7f21 100644
--- a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
@@ -63,7 +63,7 @@ export default {
v-else
role="button"
class="fa fa-times dropdown-input-search"
- @click="clearSearch"
+ @click.stop.prevent="clearSearch"
></i>
</div>
<div class="dropdown-content">
diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
index caf84dc9573..6348f32d36d 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
@@ -44,10 +44,9 @@ export default {
class="notes_holder"
>
<td
- class="notes_line"
- colspan="2"
- ></td>
- <td class="notes_content">
+ class="notes_content"
+ colspan="3"
+ >
<div class="content">
<diff-discussions
v-if="discussions.length"
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 0522e32c410..bc69ae30777 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -107,7 +107,6 @@ export default {
},
[types.EXPAND_ALL_FILES](state) {
- // eslint-disable-next-line no-param-reassign
state.diffFiles = state.diffFiles.map(file => ({
...file,
collapsed: false,
diff --git a/app/assets/javascripts/clusters/components/gcp_signup_offer.js b/app/assets/javascripts/dismissable_callout.js
index 8bc20a1c09f..5185b019376 100644
--- a/app/assets/javascripts/clusters/components/gcp_signup_offer.js
+++ b/app/assets/javascripts/dismissable_callout.js
@@ -3,8 +3,8 @@ import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import Flash from '~/flash';
-export default function gcpSignupOffer() {
- const alertEl = document.querySelector('.gcp-signup-offer');
+export default function initDismissableCallout(alertSelector) {
+ const alertEl = document.querySelector(alertSelector);
if (!alertEl) {
return;
}
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index 8b4f3b05ee7..f820f0dc3f0 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -65,8 +65,8 @@ export const hideMenu = (el) => {
const parentEl = el.parentNode;
- el.style.display = ''; // eslint-disable-line no-param-reassign
- el.style.transform = ''; // eslint-disable-line no-param-reassign
+ el.style.display = '';
+ el.style.transform = '';
el.classList.remove(IS_ABOVE_CLASS);
parentEl.classList.remove(IS_OVER_CLASS);
parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS);
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index adde8c8cdb3..73ae928b0d9 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -37,7 +37,7 @@
</script>
<template>
- <div class="groups-list-tree-container">
+ <div class="groups-list-tree-container qa-groups-list-tree-container">
<div
v-if="searchEmpty"
class="has-no-search-results"
diff --git a/app/assets/javascripts/ide/stores/modules/branches/mutations.js b/app/assets/javascripts/ide/stores/modules/branches/mutations.js
index 081ec2d4c28..0a455f4500f 100644
--- a/app/assets/javascripts/ide/stores/modules/branches/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/branches/mutations.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js
index e413e61eaaa..674782a28ca 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
index 98102a68e08..0eba9c39817 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
index 5a2213bbe89..b4be100cb07 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
import { normalizeJob } from './utils';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 0347f803757..f2bb87ac674 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request';
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index a937fb157f8..66f29824898 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-param-reassign */
import * as types from '../mutation_types';
import { sortTree } from '../utils';
import { diffModes } from '../../constants';
diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js
index 2a451ef0cd1..cd12ef87d40 100644
--- a/app/assets/javascripts/jobs/store/mutations.js
+++ b/app/assets/javascripts/jobs/store/mutations.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-param-reassign */
-
import * as types from './mutation_types';
export default {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 8124ae6201f..1e168742667 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1074,7 +1074,7 @@ export default class Notes {
addForm = false;
let lineTypeSelector = '';
rowCssToAdd =
- '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>';
+ '<tr class="notes_holder js-temp-notes-holder"><td class="notes_content" colspan="3"><div class="content"></div></td></tr>';
// In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineTypeSelector = `.${lineType}`;
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index 27ff7dea909..802be022ba6 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -148,10 +148,9 @@ export default {
</tr>
<tr class="notes_holder">
<td
- class="notes_line"
- colspan="2"
- ></td>
- <td class="notes_content">
+ class="notes_content"
+ colspan="3"
+ >
<slot></slot>
</td>
</tr>
diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js
index 47bd70537f1..069f8ce55d0 100644
--- a/app/assets/javascripts/pages/admin/application_settings/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/index.js
@@ -1,8 +1,13 @@
import initSettingsPanels from '~/settings_panels';
import projectSelect from '~/project_select';
+import UsagePingPayload from './usage_ping_payload';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
projectSelect();
+ new UsagePingPayload(
+ document.querySelector('.js-usage-ping-payload-trigger'),
+ document.querySelector('.js-usage-ping-payload'),
+ ).init();
});
diff --git a/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js b/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js
new file mode 100644
index 00000000000..9a1bc46bf4a
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js
@@ -0,0 +1,62 @@
+import axios from '../../../lib/utils/axios_utils';
+import { __ } from '../../../locale';
+import flash from '../../../flash';
+
+export default class UsagePingPayload {
+ constructor(trigger, container) {
+ this.trigger = trigger;
+ this.container = container;
+ this.isVisible = false;
+ this.isInserted = false;
+ }
+
+ init() {
+ this.spinner = this.trigger.querySelector('.js-spinner');
+ this.text = this.trigger.querySelector('.js-text');
+
+ this.trigger.addEventListener('click', event => {
+ event.preventDefault();
+
+ if (this.isVisible) return this.hidePayload();
+
+ return this.requestPayload();
+ });
+ }
+
+ requestPayload() {
+ if (this.isInserted) return this.showPayload();
+
+ this.spinner.classList.add('d-inline');
+
+ return axios
+ .get(this.container.dataset.endpoint, {
+ responseType: 'text',
+ })
+ .then(({ data }) => {
+ this.spinner.classList.remove('d-inline');
+ this.insertPayload(data);
+ })
+ .catch(() => {
+ this.spinner.classList.remove('d-inline');
+ flash(__('Error fetching usage ping data.'));
+ });
+ }
+
+ hidePayload() {
+ this.isVisible = false;
+ this.container.classList.add('d-none');
+ this.text.textContent = __('Preview payload');
+ }
+
+ showPayload() {
+ this.isVisible = true;
+ this.container.classList.remove('d-none');
+ this.text.textContent = __('Hide payload');
+ }
+
+ insertPayload(data) {
+ this.isInserted = true;
+ this.container.innerHTML = data;
+ this.showPayload();
+ }
+}
diff --git a/app/assets/javascripts/pages/instance_statistics/cohorts/index.js b/app/assets/javascripts/pages/instance_statistics/cohorts/index.js
deleted file mode 100644
index 2d5020dbef4..00000000000
--- a/app/assets/javascripts/pages/instance_statistics/cohorts/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initUsagePing from './usage_ping';
-
-document.addEventListener('DOMContentLoaded', initUsagePing);
diff --git a/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js b/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js
deleted file mode 100644
index 914a9661c27..00000000000
--- a/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import axios from '../../../lib/utils/axios_utils';
-import { __ } from '../../../locale';
-import flash from '../../../flash';
-
-export default function UsagePing() {
- const el = document.querySelector('.usage-data');
-
- axios.get(el.dataset.endpoint, {
- responseType: 'text',
- }).then(({ data }) => {
- el.innerHTML = data;
- }).catch(() => flash(__('Error fetching usage ping data.')));
-}
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index cc0e6553e83..9c074b74c3b 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,4 +1,4 @@
-import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+import initDismissableCallout from '~/dismissable_callout';
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
import Project from './project';
import ShortcutsNavigation from '../../shortcuts_navigation';
@@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
];
if (newClusterViews.indexOf(page) > -1) {
- gcpSignupOffer();
+ initDismissableCallout('.gcp-signup-offer');
initGkeDropdowns();
}
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index a853624e944..fdcbcc236c1 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -13,40 +13,52 @@ export default class Project {
constructor() {
const $cloneOptions = $('ul.clone-options-dropdown');
const $projectCloneField = $('#project_clone');
- const $cloneBtnText = $('a.clone-dropdown-btn span');
+ const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label');
- const selectedCloneOption = $cloneBtnText.text().trim();
+ const selectedCloneOption = $cloneBtnLabel.text().trim();
if (selectedCloneOption.length > 0) {
$(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
}
- $('a', $cloneOptions).on('click', (e) => {
+ $('a', $cloneOptions).on('click', e => {
+ e.preventDefault();
const $this = $(e.currentTarget);
const url = $this.attr('href');
- const activeText = $this.find('.dropdown-menu-inner-title').text();
+ const cloneType = $this.data('cloneType');
- e.preventDefault();
+ $('.is-active', $cloneOptions).removeClass('is-active');
+ $(`a[data-clone-type="${cloneType}"]`).each(function() {
+ const $el = $(this);
+ const activeText = $el.find('.dropdown-menu-inner-title').text();
+ const $container = $el.closest('.project-clone-holder');
+ const $label = $container.find('.js-clone-dropdown-label');
- $('.is-active', $cloneOptions).not($this).removeClass('is-active');
- $this.toggleClass('is-active');
- $projectCloneField.val(url);
- $cloneBtnText.text(activeText);
+ $el.toggleClass('is-active');
+ $label.text(activeText);
+ });
- return $('.clone').text(url);
+ $projectCloneField.val(url);
+ $('.js-git-empty .js-clone').text(url);
});
// Ref switcher
Project.initRefSwitcher();
$('.project-refs-select').on('change', function() {
- return $(this).parents('form').submit();
+ return $(this)
+ .parents('form')
+ .submit();
});
$('.hide-no-ssh-message').on('click', function(e) {
Cookies.set('hide_no_ssh_message', 'false');
- $(this).parents('.no-ssh-key-message').remove();
+ $(this)
+ .parents('.no-ssh-key-message')
+ .remove();
return e.preventDefault();
});
$('.hide-no-password-message').on('click', function(e) {
Cookies.set('hide_no_password_message', 'false');
- $(this).parents('.no-password-message').remove();
+ $(this)
+ .parents('.no-password-message')
+ .remove();
return e.preventDefault();
});
Project.projectSelectDropdown();
@@ -58,7 +70,7 @@ export default class Project {
}
static changeProject(url) {
- return window.location = url;
+ return (window.location = url);
}
static initRefSwitcher() {
@@ -73,14 +85,15 @@ export default class Project {
selected = $dropdown.data('selected');
return $dropdown.glDropdown({
data(term, callback) {
- axios.get($dropdown.data('refsUrl'), {
- params: {
- ref: $dropdown.data('ref'),
- search: term,
- },
- })
- .then(({ data }) => callback(data))
- .catch(() => flash(__('An error occurred while getting projects')));
+ axios
+ .get($dropdown.data('refsUrl'), {
+ params: {
+ ref: $dropdown.data('ref'),
+ search: term,
+ },
+ })
+ .then(({ data }) => callback(data))
+ .catch(() => flash(__('An error occurred while getting projects')));
},
selectable: true,
filterable: true,
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index b76f2f76449..0507f67843f 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -8,15 +8,18 @@ import BlobViewer from '~/blob/viewer/index';
import Activities from '~/activities';
import { ajaxGet } from '~/lib/utils/common_utils';
import GpgBadges from '~/gpg_badges';
+import initReadMore from '~/read_more';
import Star from '../../../star';
import notificationsDropdown from '../../../notifications_dropdown';
document.addEventListener('DOMContentLoaded', () => {
+ initReadMore();
new Star(); // eslint-disable-line no-new
notificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new
new NotificationsForm(); // eslint-disable-line no-new
- new UserCallout({ // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new UserCallout({
setCalloutPerProject: false,
className: 'js-autodevops-banner',
});
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 29b347824de..a39cc265601 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -132,10 +132,8 @@ export default {
if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'path') {
- // eslint-disable-next-line no-param-reassign
accumulator.ref_url = this.pipeline.ref[prop];
} else {
- // eslint-disable-next-line no-param-reassign
accumulator[prop] = this.pipeline.ref[prop];
}
return accumulator;
diff --git a/app/assets/javascripts/read_more.js b/app/assets/javascripts/read_more.js
new file mode 100644
index 00000000000..d2d1ac8c76a
--- /dev/null
+++ b/app/assets/javascripts/read_more.js
@@ -0,0 +1,41 @@
+/**
+ * ReadMore
+ *
+ * Adds "read more" functionality to elements.
+ *
+ * Specifically, it looks for a trigger, by default ".js-read-more-trigger", and adds the class
+ * "is-expanded" to the previous element in order to provide a click to expand functionality.
+ *
+ * This is useful for long text elements that you would like to truncate, especially for mobile.
+ *
+ * Example Markup
+ * <div class="read-more-container">
+ * <p>Some text that should be long enough to have to truncate within a specified container.</p>
+ * <p>This text will not appear in the container, as only the first line can be truncated.</p>
+ * <p>This should also not appear, if everything is working correctly!</p>
+ * </div>
+ * <button class="js-read-more-trigger">Read more</button>
+ *
+ */
+export default function initReadMore(triggerSelector = '.js-read-more-trigger') {
+ const triggerEls = document.querySelectorAll(triggerSelector);
+
+ if (!triggerEls) return;
+
+ triggerEls.forEach(triggerEl => {
+ const targetEl = triggerEl.previousElementSibling;
+
+ if (!targetEl) {
+ return;
+ }
+
+ triggerEl.addEventListener(
+ 'click',
+ e => {
+ targetEl.classList.add('is-expanded');
+ e.target.remove();
+ },
+ { once: true },
+ );
+ });
+}
diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js
index 1983a8c9e56..b88bff97075 100644
--- a/app/assets/javascripts/reports/store/mutations.js
+++ b/app/assets/javascripts/reports/store/mutations.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 72bd28ae03f..4c3f8dff3c4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -4,6 +4,7 @@ import { n__, s__, sprintf } from '~/locale';
import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default {
@@ -13,6 +14,9 @@ export default {
clipboardButton,
TooltipOnTruncate,
},
+ directives: {
+ tooltip,
+ },
props: {
mr: {
type: Object,
@@ -40,10 +44,19 @@ export default {
});
},
webIdePath() {
- return mergeUrlParams({
- target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ?
- this.mr.targetProjectFullPath : '',
- }, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`));
+ if (this.mr.canPushToSourceBranch) {
+ return mergeUrlParams({
+ target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ?
+ this.mr.targetProjectFullPath : '',
+ }, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`));
+ }
+
+ return null;
+ },
+ ideButtonTitle() {
+ return !this.mr.canPushToSourceBranch
+ ? s__('mrWidget|You are not allowed to edit this project directly. Please fork to make changes.')
+ : '';
},
},
};
@@ -93,13 +106,22 @@ export default {
v-if="mr.isOpen"
class="branch-actions"
>
- <a
- v-if="!mr.sourceBranchRemoved"
- :href="webIdePath"
- class="btn btn-default inline js-web-ide d-none d-md-inline-block"
+ <span
+ v-tooltip
+ :title="ideButtonTitle"
+ data-placement="bottom"
+ tabindex="0"
>
- {{ s__("mrWidget|Open in Web IDE") }}
- </a>
+ <a
+ v-if="!mr.sourceBranchRemoved"
+ :href="webIdePath"
+ :class="{ disabled: !mr.canPushToSourceBranch }"
+ class="btn btn-default inline js-web-ide d-none d-md-inline-block"
+ role="button"
+ >
+ {{ s__("mrWidget|Open in Web IDE") }}
+ </a>
+ </span>
<button
:disabled="mr.sourceBranchRemoved"
data-target="#modal_merge_info"
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
index 73b9131e5ba..b9693892f45 100644
--- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -28,7 +28,7 @@ Vue.http.interceptors.push((request, next) => {
response.headers.forEach((value, key) => {
headers[key] = value;
});
-
+ // eslint-disable-next-line no-param-reassign
response.headers = headers;
});
});
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index b1a20c06910..39ffabb3ea6 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -64,3 +64,4 @@
@import 'framework/ci_variable_list';
@import 'framework/feature_highlight';
@import 'framework/terms';
+@import 'framework/read_more';
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index a265e4206f1..702276780e9 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -229,8 +229,8 @@
svg {
margin-bottom: 1px;
- height: 18px;
- width: 18px;
+ height: $default-icon-size;
+ width: $default-icon-size;
border-radius: 50%;
path {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 72b4ed0ac33..e91e830fcac 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -149,7 +149,8 @@
&.btn-success,
&.btn-new,
&.btn-create,
- &.btn-save {
+ &.btn-save,
+ &.btn-register {
@include btn-green;
}
@@ -172,8 +173,7 @@
}
&.btn-info,
- &.btn-primary,
- &.btn-register {
+ &.btn-primary {
@include btn-blue;
}
@@ -248,7 +248,7 @@
.btn-terminal {
svg {
height: 14px;
- width: 18px;
+ width: $default-icon-size;
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index abfe350677e..a52e6c4f6a7 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -216,8 +216,8 @@
vertical-align: inherit;
img {
- height: 18px;
- width: 18px;
+ height: $default-icon-size;
+ width: $default-icon-size;
}
}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 6244fb86fea..6d20c46b99d 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -4,11 +4,6 @@
margin-top: 20px;
}
- .container-fluid {
- padding-left: 5px;
- padding-right: 5px;
- }
-
.nav-links > li > a {
padding: 10px;
font-size: 12px;
@@ -49,12 +44,8 @@
.project-repo-buttons {
display: block;
- .count-buttons .btn {
- margin: 0 10px;
- }
-
- .count-buttons .count-with-arrow {
- display: none;
+ .count-buttons .count-badge {
+ margin-top: $gl-padding-8;
}
}
}
diff --git a/app/assets/stylesheets/framework/read_more.scss b/app/assets/stylesheets/framework/read_more.scss
new file mode 100644
index 00000000000..b84b6e0b256
--- /dev/null
+++ b/app/assets/stylesheets/framework/read_more.scss
@@ -0,0 +1,13 @@
+.read-more-container {
+ @include media-breakpoint-down(md) {
+ &:not(.is-expanded) {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ > * {
+ display: inline;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss
index 20394cc1e52..43aaf198609 100644
--- a/app/assets/stylesheets/framework/toggle.scss
+++ b/app/assets/stylesheets/framework/toggle.scss
@@ -56,8 +56,8 @@
&,
.toggle-icon-svg {
- width: 18px;
- height: 18px;
+ width: $default-icon-size;
+ height: $default-icon-size;
}
.toggle-icon-svg {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index d76f5cbd9ff..f5e7a84d082 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -250,7 +250,7 @@ $container-text-max-width: 540px;
$gl-avatar-size: 40px;
$border-radius-default: 4px;
$border-radius-small: 2px;
-$settings-icon-size: 18px;
+$default-icon-size: 18px;
$layout-link-gray: #7e7c7c;
$btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
@@ -271,6 +271,7 @@ $performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
$breadcrumb-min-height: 48px;
+$project-title-row-height: 24px;
/*
* Common component specific colors
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index 6c555aee20a..f0acb78f731 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -4,3 +4,7 @@
padding-bottom: 46px;
}
}
+
+.usage-data {
+ max-height: 400px;
+}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 7d7143631f2..d673b59e1c0 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -749,6 +749,10 @@
left: $gl-padding;
}
+ .dropdown-input .dropdown-input-search {
+ pointer-events: all;
+ }
+
.diff-changed-file {
display: flex;
padding-top: 8px;
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index c9e5fb9c579..fa0ab1a3bae 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -100,6 +100,22 @@
p {
margin: 0;
}
+
+ .omniauth-btn {
+ margin-bottom: $gl-padding;
+ width: 48%;
+ padding: $gl-padding-8;
+
+ @include media-breakpoint-down(md) {
+ width: 100%;
+ }
+
+ img {
+ width: $default-icon-size;
+ height: $default-icon-size;
+ margin-right: $gl-padding;
+ }
+ }
}
.new-session-tabs {
@@ -169,10 +185,6 @@
}
}
- label {
- font-weight: $gl-font-weight-normal;
- }
-
.submit-container {
margin-top: 16px;
}
@@ -200,15 +212,6 @@
}
}
-.oauth-image-link {
- margin-right: 10px;
-
- img {
- width: 32px;
- height: 32px;
- }
-}
-
.devise-layout-html {
margin: 0;
padding: 0;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index dbe9f0c03fb..b1e33196049 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -334,20 +334,6 @@ ul.notes {
border: 1px solid $white-normal;
border-left: 0;
- &.notes_line {
- vertical-align: middle;
- text-align: center;
- padding: 10px 0;
- background: $gray-light;
- color: $text-color;
- }
-
- &.notes_line2 {
- text-align: center;
- padding: 10px 0;
- border-left: 1px solid $note-line2-border !important;
- }
-
&.notes_content {
background-color: $gray-light;
border-width: 1px 0;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index a95e78931b1..9b7051924e6 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -115,7 +115,7 @@
.project-feature-controls {
display: flex;
align-items: center;
- margin: 8px 0;
+ margin: $gl-padding-8 0;
max-width: 432px;
.toggle-wrapper {
@@ -144,12 +144,8 @@
.group-home-panel {
padding-top: 24px;
padding-bottom: 24px;
+ border-bottom: 1px solid $border-color;
- @include media-breakpoint-up(sm) {
- border-bottom: 1px solid $border-color;
- }
-
- .project-avatar,
.group-avatar {
float: none;
margin: 0 auto;
@@ -175,7 +171,6 @@
}
}
- .project-home-desc,
.group-home-desc {
margin-left: auto;
margin-right: auto;
@@ -199,6 +194,62 @@
}
}
+.project-home-panel {
+ padding-top: $gl-padding-8;
+ padding-bottom: $gl-padding-24;
+
+ .project-title-row {
+ margin-right: $gl-padding-8;
+ }
+
+ .project-avatar {
+ width: $project-title-row-height;
+ height: $project-title-row-height;
+ flex-shrink: 0;
+ flex-basis: $project-title-row-height;
+ margin: 0 $gl-padding-8 0 0;
+ }
+
+ .project-title {
+ font-size: 20px;
+ line-height: $project-title-row-height;
+ font-weight: bold;
+ }
+
+ .project-metadata {
+ font-weight: normal;
+ font-size: 14px;
+ line-height: $gl-btn-line-height;
+ color: $gl-text-color-secondary;
+
+ .icon {
+ margin-right: $gl-padding-4;
+ font-size: 16px;
+ }
+
+ .project-visibility,
+ .project-license,
+ .project-tag-list {
+ margin-right: $gl-padding-8;
+ }
+
+ .project-license {
+ .btn {
+ line-height: 0;
+ border-width: 0;
+ }
+ }
+
+ .project-tag-list,
+ .project-license {
+ .icon {
+ position: relative;
+ top: 2px;
+ }
+ }
+ }
+}
+
.nav > .project-repo-buttons {
margin-top: 0;
}
@@ -206,8 +257,6 @@
.project-repo-buttons,
.group-buttons {
.btn {
- padding: 3px 10px;
-
&:last-child {
margin-left: 0;
}
@@ -222,11 +271,15 @@
.fa-caret-down {
margin-left: 3px;
+
+ &.dropdown-btn-icon {
+ margin-left: 0;
+ }
}
}
.project-action-button {
- margin: 15px 5px 0;
+ margin: $gl-padding $gl-padding-8 0 0;
vertical-align: top;
}
@@ -243,82 +296,45 @@
.count-buttons {
display: inline-block;
vertical-align: top;
- margin-top: 15px;
- }
+ margin-top: $gl-padding;
- .project-clone-holder {
- display: inline-block;
- margin: 15px 5px 0 0;
+ .count-badge {
+ height: $input-height;
- input {
- height: 28px;
+ .icon {
+ top: -1px;
+ }
}
- }
- .count-with-arrow {
- display: inline-block;
- position: relative;
- margin-left: 4px;
+ .count-badge-count,
+ .count-badge-button {
+ border: 1px solid $border-color;
+ line-height: 1;
+ }
- .arrow {
- &::before {
- content: '';
- display: inline-block;
- position: absolute;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
- top: 50%;
- left: 0;
- margin-top: -6px;
- border-width: 7px 5px 7px 0;
- border-right-color: $count-arrow-border;
- pointer-events: none;
- }
+ .count,
+ .count-badge-button {
+ color: $gl-text-color;
+ }
- &::after {
- content: '';
- position: absolute;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
- top: 50%;
- left: 1px;
- margin-top: -9px;
- border-width: 10px 7px 10px 0;
- border-right-color: $white-light;
- pointer-events: none;
- }
+ .count-badge-count {
+ padding: 0 12px;
+ border-right: 0;
+ border-radius: $border-radius-base 0 0 $border-radius-base;
+ background: $gray-light;
}
- .count {
- @include btn-white;
- display: inline-block;
- background: $white-light;
- border-radius: 2px;
- border-width: 1px;
- border-style: solid;
- font-size: 13px;
- font-weight: $gl-font-weight-bold;
- line-height: 13px;
- letter-spacing: 0.4px;
- padding: 6px 14px;
- text-align: center;
- vertical-align: middle;
- touch-action: manipulation;
- background-image: none;
- white-space: nowrap;
- margin: 0 10px 0 4px;
+ .count-badge-button {
+ border-radius: 0 $border-radius-base $border-radius-base 0;
+ }
+ }
- a {
- color: inherit;
- }
+ .project-clone-holder {
+ display: inline-block;
+ margin: $gl-padding $gl-padding-8 0 0;
- &:hover {
- background: $white-light;
- }
+ input {
+ height: $input-height;
}
}
@@ -333,6 +349,14 @@
min-width: 320px;
}
}
+
+ .mobile-git-clone {
+ margin-top: $gl-padding-8;
+
+ .dropdown-menu-inner-content {
+ @extend .monospace;
+ }
+ }
}
.split-one {
@@ -511,7 +535,6 @@
.controls {
margin-left: auto;
}
-
}
.choose-template {
@@ -574,7 +597,7 @@
flex-wrap: wrap;
.btn {
- padding: 8px;
+ padding: $gl-padding-8;
margin-right: 10px;
}
@@ -651,7 +674,7 @@
left: -10px;
top: 50%;
z-index: 10;
- padding: 8px 0;
+ padding: $gl-padding-8 0;
text-align: center;
background-color: $white-light;
color: $gl-text-color-tertiary;
@@ -665,7 +688,7 @@
left: 50%;
top: 0;
transform: translateX(-50%);
- padding: 0 8px;
+ padding: 0 $gl-padding-8;
}
}
@@ -699,17 +722,51 @@
.project-stats {
font-size: 0;
text-align: center;
- max-width: 100%;
border-bottom: 1px solid $border-color;
- .nav {
- margin-top: $gl-padding-8;
- margin-bottom: $gl-padding-8;
+ .scrolling-tabs-container {
+ .scrolling-tabs {
+ margin-top: $gl-padding-8;
+ margin-bottom: $gl-padding-8;
+ flex-wrap: wrap;
+ border-bottom: 0;
+ }
+ .fade-left,
+ .fade-right {
+ top: 0;
+ height: 100%;
+
+ .fa {
+ top: 50%;
+ margin-top: -$gl-padding-8;
+ }
+ }
+
+ .nav {
+ flex-basis: 100%;
+
+ + .nav {
+ margin: $gl-padding-8 0;
+ }
+ }
+
+ @include media-breakpoint-down(md) {
+ flex-direction: column;
+
+ .nav {
+ flex-wrap: nowrap;
+ }
+
+ .nav:first-child {
+ margin-right: $gl-padding-8;
+ }
+ }
+ }
+
+ .nav {
> li {
display: inline-block;
- margin-top: $gl-padding-4;
- margin-bottom: $gl-padding-4;
&:not(:last-child) {
margin-right: $gl-padding;
@@ -732,13 +789,17 @@
font-size: $gl-font-size;
line-height: $gl-btn-line-height;
color: $gl-text-color-secondary;
+ white-space: nowrap;
}
.stat-link {
+ border-bottom: 0;
+
&:hover,
&:focus {
color: $gl-text-color;
text-decoration: underline;
+ border-bottom: 0;
}
}
@@ -868,7 +929,7 @@ pre.light-well {
}
.git-clone-holder {
- width: 380px;
+ width: 320px;
.btn-clipboard {
border: 1px solid $border-color;
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index e351dd7c0bb..5a594920e44 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -106,7 +106,7 @@
.settings-list-icon {
color: $gl-text-color-secondary;
- font-size: $settings-icon-size;
+ font-size: $default-icon-size;
line-height: 42px;
}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index e5b38898a67..7cd68d6b92a 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -110,6 +110,7 @@ class ApplicationController < ActionController::Base
def append_info_to_payload(payload)
super
+ payload[:ua] = request.env["HTTP_USER_AGENT"]
payload[:remote_ip] = request.remote_ip
logged_user = auth_user
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 22b39f47bf0..a2c96f5d635 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -1,5 +1,6 @@
module IssuableCollections
extend ActiveSupport::Concern
+ include CookiesHelper
include SortingHelper
include Gitlab::IssuableMetadata
include Gitlab::Utils::StrongMemoize
@@ -107,11 +108,14 @@ module IssuableCollections
end
def set_sort_order_from_cookie
- cookies[remember_sorting_key] = params[:sort] if params[:sort].present?
+ sort_param = params[:sort] if params[:sort].present?
# fallback to legacy cookie value for backward compatibility
- cookies[remember_sorting_key] ||= cookies['issuable_sort']
- cookies[remember_sorting_key] = update_cookie_value(cookies[remember_sorting_key])
- params[:sort] = cookies[remember_sorting_key]
+ sort_param ||= cookies['issuable_sort']
+ sort_param ||= cookies[remember_sorting_key]
+
+ sort_value = update_cookie_value(sort_param)
+ set_secure_cookie(remember_sorting_key, sort_value)
+ params[:sort] = sort_value
end
def remember_sorting_key
diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb
index 237c93daee8..382ec91f771 100644
--- a/app/controllers/concerns/send_file_upload.rb
+++ b/app/controllers/concerns/send_file_upload.rb
@@ -1,7 +1,11 @@
module SendFileUpload
def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment')
if attachment
- redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}" }
+ # Response-Content-Type will not override an existing Content-Type in
+ # Google Cloud Storage, so the metadata needs to be cleared on GCS for
+ # this to work. However, this override works with AWS.
+ redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}",
+ "response-content-type" => guess_content_type(attachment) }
# By default, Rails will send uploads with an extension of .js with a
# content-type of text/javascript, which will trigger Rails'
# cross-origin JavaScript protection.
@@ -18,4 +22,14 @@ module SendFileUpload
redirect_to file_upload.url(**redirect_params)
end
end
+
+ def guess_content_type(filename)
+ types = MIME::Types.type_for(filename)
+
+ if types.present?
+ types.first.content_type
+ else
+ "application/octet-stream"
+ end
+ end
end
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index 434459a225a..bb8c245a5b7 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -53,6 +53,8 @@ module UploadsActions
maximum_size: Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i)
render json: authorized
+ rescue SocketError
+ render json: "Error uploading file", status: :internal_server_error
end
private
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index 863f50e8e66..3e0076ac935 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -11,7 +11,7 @@ class Groups::LabelsController < Groups::ApplicationController
def index
respond_to do |format|
format.html do
- @labels = @group.labels.page(params[:page])
+ @labels = @available_labels.page(params[:page])
end
format.json do
render json: LabelSerializer.new.represent_appearance(@available_labels)
@@ -113,7 +113,7 @@ class Groups::LabelsController < Groups::ApplicationController
group_id: @group.id,
only_group_labels: params[:only_group_labels],
include_ancestor_groups: params[:include_ancestor_groups],
- include_descendant_groups: params[:include_descendant_groups]
- ).execute
+ include_descendant_groups: params[:include_descendant_groups],
+ search: params[:search]).execute
end
end
diff --git a/app/controllers/instance_statistics/cohorts_controller.rb b/app/controllers/instance_statistics/cohorts_controller.rb
index 7eba0a5ecdd..4b4e39db2e1 100644
--- a/app/controllers/instance_statistics/cohorts_controller.rb
+++ b/app/controllers/instance_statistics/cohorts_controller.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationController
+ before_action :authenticate_usage_ping_enabled_or_admin!
+
def index
if Gitlab::CurrentSettings.usage_ping_enabled
cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
@@ -10,4 +12,8 @@ class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationCon
@cohorts = CohortsSerializer.new.represent(cohorts_results)
end
end
+
+ def authenticate_usage_ping_enabled_or_admin!
+ render_404 unless Gitlab::CurrentSettings.usage_ping_enabled || current_user.admin?
+ end
end
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index b4f814fd3a4..695ffd90a85 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -1,4 +1,5 @@
class Projects::ApplicationController < ApplicationController
+ include CookiesHelper
include RoutableActions
include ChecksCollaboration
@@ -74,7 +75,7 @@ class Projects::ApplicationController < ApplicationController
end
def apply_diff_view_cookie!
- cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present?
+ set_secure_cookie(:diff_view, params.delete(:view), permanent: true) if params[:view].present?
end
def require_pages_enabled!
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 358fe59618b..b4fd09c06e5 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -157,7 +157,8 @@ class Projects::ClustersController < Projects::ApplicationController
:namespace,
:api_url,
:token,
- :ca_cert
+ :ca_cert,
+ :authorization_type
]).merge(
provider_type: :user,
platform_type: :kubernetes
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index 2da2aad9b33..bbf8c7d5cbc 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -66,6 +66,7 @@ class Projects::HooksController < Projects::ApplicationController
:enable_ssl_verification,
:token,
:url,
+ :push_events_branch_filter,
*ProjectHook.triggers.values
)
end
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 48a09e1ddb8..0fed7f6576c 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -36,54 +36,47 @@ class Projects::RefsController < Projects::ApplicationController
end
def logs_tree
- @offset = if params[:offset].present?
- params[:offset].to_i
- else
- 0
- end
+ summary = ::Gitlab::TreeSummary.new(
+ @commit,
+ @project,
+ path: @path,
+ offset: params[:offset],
+ limit: 25
+ )
- @limit = 25
-
- @path = params[:path]
-
- contents = []
- contents.push(*tree.trees)
- contents.push(*tree.blobs)
- contents.push(*tree.submodules)
-
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433
- @logs = Gitlab::GitalyClient.allow_n_plus_1_calls do
- contents[@offset, @limit].to_a.map do |content|
- file = @path ? File.join(@path, content.name) : content.name
- last_commit = @repo.last_commit_for_path(@commit.id, file)
- commit_path = project_commit_path(@project, last_commit) if last_commit
- {
- file_name: content.name,
- commit: last_commit,
- type: content.type,
- commit_path: commit_path
- }
- end
- end
-
- offset = (@offset + @limit)
- if contents.size > offset
- @more_log_url = logs_file_project_ref_path(@project, @ref, @path || '', offset: offset)
- end
+ @logs, commits = summary.summarize
+ @more_log_url = more_url(summary.next_offset) if summary.more?
respond_to do |format|
format.html { render_404 }
format.json do
- response.headers["More-Logs-Url"] = @more_log_url
-
+ response.headers["More-Logs-Url"] = @more_log_url if summary.more?
render json: @logs
end
- format.js
+
+ # The commit titles must be rendered and redacted before being shown.
+ # Doing it here allows us to apply performance optimizations that avoid
+ # N+1 problems
+ format.js do
+ prerender_commit_full_titles!(commits)
+ end
end
end
private
+ def more_url(offset)
+ logs_file_project_ref_path(@project, @ref, @path, offset: offset)
+ end
+
+ def prerender_commit_full_titles!(commits)
+ # Preload commit authors as they are used in rendering
+ commits.each(&:lazy_author)
+
+ renderer = Banzai::ObjectRenderer.new(user: current_user, default_project: @project)
+ renderer.render(commits, :full_title)
+ end
+
def validate_ref_id
return not_found! if params[:id].present? && params[:id] !~ Gitlab::PathRegex.git_reference_regex
end
diff --git a/app/finders/template_finder.rb b/app/finders/template_finder.rb
new file mode 100644
index 00000000000..ea0251bffb6
--- /dev/null
+++ b/app/finders/template_finder.rb
@@ -0,0 +1,37 @@
+class TemplateFinder
+ VENDORED_TEMPLATES = {
+ dockerfiles: ::Gitlab::Template::DockerfileTemplate,
+ gitignores: ::Gitlab::Template::GitignoreTemplate,
+ gitlab_ci_ymls: ::Gitlab::Template::GitlabCiYmlTemplate
+ }.freeze
+
+ class << self
+ def build(type, params = {})
+ if type == :licenses
+ LicenseTemplateFinder.new(params)
+ else
+ new(type, params)
+ end
+ end
+ end
+
+ attr_reader :type, :params
+
+ attr_reader :vendored_templates
+ private :vendored_templates
+
+ def initialize(type, params = {})
+ @type = type
+ @params = params
+
+ @vendored_templates = VENDORED_TEMPLATES.fetch(type)
+ end
+
+ def execute
+ if params[:name]
+ vendored_templates.find(params[:name])
+ else
+ vendored_templates.all
+ end
+ end
+end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 00ebafd177b..96f7415ae98 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -158,32 +158,35 @@ module BlobHelper
end
def licenses_for_select
- return @licenses_for_select if defined?(@licenses_for_select)
-
- grouped_licenses = LicenseTemplateFinder.new.execute.group_by(&:category)
- categories = grouped_licenses.keys
-
- @licenses_for_select = categories.each_with_object({}) do |category, hash|
- hash[category] = grouped_licenses[category].map do |license|
- { name: license.name, id: license.id }
- end
- end
+ @licenses_for_select ||= template_dropdown_names(TemplateFinder.build(:licenses).execute)
end
def ref_project
@ref_project ||= @target_project || @project
end
+ def template_dropdown_names(items)
+ grouped = items.group_by(&:category)
+ categories = grouped.keys
+
+ categories.each_with_object({}) do |category, hash|
+ hash[category] = grouped[category].map do |item|
+ { name: item.name, id: item.id }
+ end
+ end
+ end
+ private :template_dropdown_names
+
def gitignore_names
- @gitignore_names ||= Gitlab::Template::GitignoreTemplate.dropdown_names
+ @gitignore_names ||= template_dropdown_names(TemplateFinder.build(:gitignores).execute)
end
def gitlab_ci_ymls
- @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names(params[:context])
+ @gitlab_ci_ymls ||= template_dropdown_names(TemplateFinder.build(:gitlab_ci_ymls).execute)
end
def dockerfile_names
- @dockerfile_names ||= Gitlab::Template::DockerfileTemplate.dropdown_names
+ @dockerfile_names ||= template_dropdown_names(TemplateFinder.build(:dockerfiles).execute)
end
def blob_editor_paths
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 26e3850a540..2b3fe57767c 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -61,7 +61,7 @@ module ButtonHelper
dropdown_description = http_dropdown_description(protocol)
append_url = project.http_url_to_repo if append_link
- dropdown_item_with_description(protocol, dropdown_description, href: append_url)
+ dropdown_item_with_description(protocol, dropdown_description, href: append_url, data: { clone_type: 'http' })
end
def http_dropdown_description(protocol)
@@ -80,16 +80,17 @@ module ButtonHelper
append_url = project.ssh_url_to_repo if append_link
- dropdown_item_with_description('SSH', dropdown_description, href: append_url)
+ dropdown_item_with_description('SSH', dropdown_description, href: append_url, data: { clone_type: 'ssh' })
end
- def dropdown_item_with_description(title, description, href: nil)
+ def dropdown_item_with_description(title, description, href: nil, data: nil)
button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title')
button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description
content_tag (href ? :a : :span),
(href ? button_content : title),
class: "#{title.downcase}-selector",
- href: (href if href)
+ href: (href if href),
+ data: (data if data)
end
end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index 8fd0b6f14c6..73049c74d80 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -11,4 +11,8 @@ module ClustersHelper
render 'projects/clusters/gcp_signup_offer_banner'
end
end
+
+ def rbac_clusters_feature_enabled?
+ Feature.enabled?(:rbac_clusters)
+ end
end
diff --git a/app/helpers/cookies_helper.rb b/app/helpers/cookies_helper.rb
new file mode 100644
index 00000000000..3a7e9987190
--- /dev/null
+++ b/app/helpers/cookies_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module CookiesHelper
+ def set_secure_cookie(key, value, httponly: false, permanent: false)
+ cookie_jar = permanent ? cookies.permanent : cookies
+
+ cookie_jar[key] = { value: value, secure: Gitlab.config.gitlab.https, httponly: httponly }
+ end
+end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index cb6f709c604..269acf5b2e2 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -110,10 +110,12 @@ module EventsHelper
event.note_target)
elsif event.note?
if event.note_target
- event_note_target_path(event)
+ event_note_target_url(event)
end
elsif event.push?
push_event_feed_url(event)
+ elsif event.created_project?
+ project_url(event.project)
end
end
@@ -145,14 +147,14 @@ module EventsHelper
end
end
- def event_note_target_path(event)
+ def event_note_target_url(event)
if event.commit_note?
- project_commit_path(event.project, event.note_target, anchor: dom_id(event.target))
+ project_commit_url(event.project, event.note_target, anchor: dom_id(event.target))
elsif event.project_snippet_note?
- project_snippet_path(event.project, event.note_target, anchor: dom_id(event.target))
+ project_snippet_url(event.project, event.note_target, anchor: dom_id(event.target))
else
- polymorphic_path([event.project.namespace.becomes(Namespace),
- event.project, event.note_target],
+ polymorphic_url([event.project.namespace.becomes(Namespace),
+ event.project, event.note_target],
anchor: dom_id(event.target))
end
end
@@ -166,7 +168,7 @@ module EventsHelper
event.note_target_reference
end
- link_to(text, event_note_target_path(event), title: event.target_title, class: 'has-tooltip')
+ link_to(text, event_note_target_url(event), title: event.target_title, class: 'has-tooltip')
else
content_tag(:strong, '(deleted)')
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index a8a10c98d69..a5612372aa6 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -86,7 +86,7 @@ module IconsHelper
end
end
- def visibility_level_icon(level, fw: true)
+ def visibility_level_icon(level, fw: true, options: {})
name =
case level
when Gitlab::VisibilityLevel::PRIVATE
@@ -99,7 +99,7 @@ module IconsHelper
name << " fw" if fw
- icon(name)
+ icon(name, options)
end
def file_type_icon_class(type, mode, name)
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index cbb971cf8b7..3adaa1366c0 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -107,23 +107,23 @@ module MarkupHelper
def markup(file_name, text, context = {})
context[:project] ||= @project
- context[:markdown_engine] ||= :redcarpet
+ context[:markdown_engine] ||= :redcarpet unless commonmark_for_repositories_enabled?
html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
prepare_for_rendering(html, context)
end
- def render_wiki_content(wiki_page)
+ def render_wiki_content(wiki_page, context = {})
text = wiki_page.content
return '' unless text.present?
- context = {
+ context.merge!(
pipeline: :wiki,
project: @project,
project_wiki: @project_wiki,
page_slug: wiki_page.slug,
- issuable_state_filter_enabled: true,
- markdown_engine: :redcarpet
- }
+ issuable_state_filter_enabled: true
+ )
+ context[:markdown_engine] ||= :redcarpet unless commonmark_for_repositories_enabled?
html =
case wiki_page.format
@@ -178,6 +178,10 @@ module MarkupHelper
end
end
+ def commonmark_for_repositories_enabled?
+ Feature.enabled?(:commonmark_for_repositories, default_enabled: true)
+ end
+
private
# Return +text+, truncated to +max_chars+ characters, excluding any HTML
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 18b3badda8d..80b45176a62 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -252,6 +252,10 @@ module ProjectsHelper
"xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}"
end
+ def legacy_render_context(params)
+ params[:legacy_render] ? { markdown_engine: :redcarpet } : {}
+ end
+
private
def get_project_nav_tabs(project, current_user)
@@ -351,6 +355,10 @@ module ProjectsHelper
end
end
+ def default_clone_label
+ _("Copy %{protocol} clone URL") % { protocol: default_clone_protocol.upcase }
+ end
+
def default_clone_protocol
if allowed_protocols_present?
enabled_protocol
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index da5fe25c07d..657a3227dd4 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -1,6 +1,7 @@
module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze
+ CLUSTER_SECURITY_WARNING = 'cluster_security_warning'.freeze
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
@@ -11,6 +12,10 @@ module UserCalloutsHelper
!user_dismissed?(GCP_SIGNUP_OFFER)
end
+ def show_cluster_security_warning?
+ !user_dismissed?(CLUSTER_SECURITY_WARNING)
+ end
+
private
def user_dismissed?(feature_name)
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index cf2fe5a2019..7b64869c9ea 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -138,7 +138,7 @@ module VisibilityLevelHelper
end
def project_visibility_icon_description(level)
- "#{visibility_level_label(level)} - #{project_visibility_level_description(level)}"
+ "#{project_visibility_level_description(level)}"
end
def visibility_level_label(level)
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index 41f9eedd4bd..17940aeb900 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -1,4 +1,6 @@
module WikiHelper
+ include API::Helpers::RelatedResourcesHelpers
+
# Produces a pure text breadcrumb for a given page.
#
# page_slug - The slug of a WikiPage object.
@@ -39,4 +41,8 @@ module WikiHelper
end
end
end
+
+ def wiki_attachment_upload_url
+ expose_url(api_v4_projects_wikis_attachments_path(id: @project.id))
+ end
end
diff --git a/app/mailers/emails/auto_devops.rb b/app/mailers/emails/auto_devops.rb
new file mode 100644
index 00000000000..9705a3052d4
--- /dev/null
+++ b/app/mailers/emails/auto_devops.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Emails
+ module AutoDevops
+ def autodevops_disabled_email(pipeline, recipient)
+ @pipeline = pipeline
+ @project = pipeline.project
+
+ add_project_headers
+
+ mail(to: recipient,
+ subject: auto_devops_disabled_subject(@project.name)) do |format|
+ format.html { render layout: 'mailer' }
+ format.text { render layout: 'mailer' }
+ end
+ end
+
+ private
+
+ def auto_devops_disabled_subject(project_name)
+ subject("Auto DevOps pipeline was disabled for #{project_name}")
+ end
+ end
+end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index f4eeb85270e..f7347ee61b4 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -12,6 +12,7 @@ class Notify < BaseMailer
include Emails::Profile
include Emails::Pipelines
include Emails::Members
+ include Emails::AutoDevops
helper MergeRequestsHelper
helper DiffHelper
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index df470930e9e..c133f4e6dbb 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -125,6 +125,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
end
+ def autodevops_disabled_email
+ Notify.autodevops_disabled_email(pipeline, user.email).message
+ end
+
private
def project
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 17b7ee4f07e..93fc1b145b2 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -48,6 +48,20 @@ module Ci
gzip: 3
}
+ # `file_location` indicates where actual files are stored.
+ # Ideally, actual files should be stored in the same directory, and use the same
+ # convention to generate its path. However, sometimes we can't do so due to backward-compatibility.
+ #
+ # legacy_path ... The actual file is stored at a path consists of a timestamp
+ # and raw project/model IDs. Those rows were migrated from
+ # `ci_builds.artifacts_file` and `ci_builds.artifacts_metadata`
+ # hashed_path ... The actual file is stored at a path consists of a SHA2 based on the project ID.
+ # This is the default value.
+ enum file_location: {
+ legacy_path: 1,
+ hashed_path: 2
+ }
+
FILE_FORMAT_ADAPTERS = {
gzip: Gitlab::Ci::Build::Artifacts::GzipFileAdapter
}.freeze
@@ -72,6 +86,12 @@ module Ci
[nil, ::JobArtifactUploader::Store::LOCAL].include?(self.file_store)
end
+ def hashed_path?
+ return true if trace? # ArchiveLegacyTraces background migration might not have `file_location` column
+
+ super || self.file_location.nil?
+ end
+
def expire_in
expire_at - Time.now if expire_at
end
@@ -108,7 +128,7 @@ module Ci
end
def update_project_statistics_after_destroy
- update_project_statistics(-self.size)
+ update_project_statistics(-self.size.to_i)
end
def update_project_statistics(difference)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 526bf7af99b..2955e0b2bca 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -161,6 +161,12 @@ module Ci
PipelineNotificationWorker.perform_async(pipeline.id)
end
end
+
+ after_transition any => [:failed] do |pipeline|
+ next unless pipeline.auto_devops_source?
+
+ pipeline.run_after_commit { AutoDevops::DisableWorker.perform_async(pipeline.id) }
+ end
end
scope :internal, -> { where(source: internal_sources) }
diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb
index 55bbf7cae7e..423071ec024 100644
--- a/app/models/clusters/applications/helm.rb
+++ b/app/models/clusters/applications/helm.rb
@@ -32,7 +32,8 @@ module Clusters
def install_command
Gitlab::Kubernetes::Helm::InitCommand.new(
name: name,
- files: files
+ files: files,
+ rbac: cluster.platform_kubernetes_rbac?
)
end
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 93f654e0638..bd0286ee3f9 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -39,6 +39,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files
)
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
index ef1c76c03bd..3d84eeed5a8 100644
--- a/app/models/clusters/applications/jupyter.rb
+++ b/app/models/clusters/applications/jupyter.rb
@@ -40,6 +40,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
repository: repository
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 88399dbbb95..46d0388a464 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -48,6 +48,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files
)
@@ -71,7 +72,7 @@ module Clusters
private
def kube_client
- cluster&.kubeclient
+ cluster&.kubeclient&.core_client
end
end
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index bde255723c8..a4a2e2b79a6 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -33,6 +33,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
repository: repository
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 7cf75403ab6..d7011ef447a 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -42,6 +42,7 @@ module Clusters
delegate :on_creation?, to: :provider, allow_nil: true
delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true
+ delegate :rbac?, to: :platform_kubernetes, prefix: true, allow_nil: true
delegate :installed?, to: :application_helm, prefix: true, allow_nil: true
delegate :installed?, to: :application_ingress, prefix: true, allow_nil: true
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index e6ddca0d5d0..3a335909101 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -5,6 +5,7 @@ module Clusters
class Kubernetes < ActiveRecord::Base
include Gitlab::Kubernetes
include ReactiveCaching
+ include EnumWithNil
self.table_name = 'cluster_platforms_kubernetes'
self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.id] }
@@ -47,6 +48,12 @@ module Clusters
alias_method :active?, :enabled?
+ enum_with_nil authorization_type: {
+ unknown_authorization: nil,
+ rbac: 1,
+ abac: 2
+ }
+
def actual_namespace
if namespace.present?
namespace
@@ -95,7 +102,7 @@ module Clusters
end
def kubeclient
- @kubeclient ||= build_kubeclient!
+ @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io'])
end
private
@@ -115,15 +122,16 @@ module Clusters
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
- def build_kubeclient!(api_path: 'api', api_version: 'v1')
+ def build_kube_client!(api_groups: ['api'], api_version: 'v1')
raise "Incomplete settings" unless api_url && actual_namespace
unless (username && password) || token
raise "Either username/password or token is required to access API"
end
- ::Kubeclient::Client.new(
- join_api_url(api_path),
+ Gitlab::Kubernetes::KubeClient.new(
+ api_url,
+ api_groups,
api_version,
auth_options: kubeclient_auth_options,
ssl_options: kubeclient_ssl_options,
@@ -133,7 +141,7 @@ module Clusters
# Returns a hash of all pods in the namespace
def read_pods
- kubeclient = build_kubeclient!
+ kubeclient = build_kube_client!
kubeclient.get_pods(namespace: actual_namespace).as_json
rescue Kubeclient::HttpError => err
@@ -157,15 +165,6 @@ module Clusters
{ bearer_token: token }
end
- def join_api_url(api_path)
- url = URI.parse(api_url)
- prefix = url.path.sub(%r{/+\z}, '')
-
- url.path = [prefix, api_path].join("/")
-
- url.to_s
- end
-
def terminal_auth
{
token: token,
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 594972ad344..49c36ad9d3f 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -22,6 +22,7 @@ class Commit
attr_accessor :project, :author
attr_accessor :redacted_description_html
attr_accessor :redacted_title_html
+ attr_accessor :redacted_full_title_html
attr_reader :gpg_commit
DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index c0233661a9b..0d5311a9985 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -9,7 +9,7 @@ module Avatarable
include Gitlab::Utils::StrongMemoize
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
- validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
+ validates :avatar, file_size: { maximum: 200.kilobytes.to_i }, if: :avatar_changed?
mount_uploader :avatar, AvatarUploader
diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb
index 6e80365ee5b..c93b6589ee7 100644
--- a/app/models/concerns/case_sensitivity.rb
+++ b/app/models/concerns/case_sensitivity.rb
@@ -9,23 +9,46 @@ module CaseSensitivity
#
# Unlike other ActiveRecord methods this method only operates on a Hash.
def iwhere(params)
- criteria = self
- cast_lower = Gitlab::Database.postgresql?
+ criteria = self
params.each do |key, value|
- column = ActiveRecord::Base.connection.quote_table_name(key)
+ criteria = case value
+ when Array
+ criteria.where(value_in(key, value))
+ else
+ criteria.where(value_equal(key, value))
+ end
+ end
+
+ criteria
+ end
- condition =
- if cast_lower
- "LOWER(#{column}) = LOWER(:value)"
- else
- "#{column} = :value"
- end
+ private
+
+ def value_equal(column, value)
+ lower_value = lower_value(value)
+
+ lower_column(arel_table[column]).eq(lower_value).to_sql
+ end
- criteria = criteria.where(condition, value: value)
+ def value_in(column, values)
+ lower_values = values.map do |value|
+ lower_value(value)
end
- criteria
+ lower_column(arel_table[column]).in(lower_values).to_sql
+ end
+
+ def lower_value(value)
+ return value if Gitlab::Database.mysql?
+
+ Arel::Nodes::NamedFunction.new('LOWER', [Arel::Nodes.build_quoted(value)])
+ end
+
+ def lower_column(column)
+ return column if Gitlab::Database.mysql?
+
+ column.lower
end
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index f881ce2321c..7f14d78e976 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -49,7 +49,7 @@ module Issuable
end
end
- has_many :label_links, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :label_links, as: :target, dependent: :destroy, inverse_of: :target # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index e62e680af6e..af387c99f3d 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -50,14 +50,20 @@ module ProtectedRef
.map(&:"#{action}_access_levels").flatten
end
+ # Returns all protected refs that match the given ref name.
+ # This checks all records from the scope built up so far, and does
+ # _not_ return a relation.
+ #
+ # This method optionally takes in a list of `protected_refs` to search
+ # through, to avoid calling out to the database.
def matching(ref_name, protected_refs: nil)
- ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs)
+ (protected_refs || self.all).select { |protected_ref| protected_ref.matches?(ref_name) }
end
end
private
def ref_matcher
- @ref_matcher ||= ProtectedRefMatcher.new(self)
+ @ref_matcher ||= RefMatcher.new(self.name)
end
end
diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb
index 223a61119e5..c52baa0524c 100644
--- a/app/models/concerns/triggerable_hooks.rb
+++ b/app/models/concerns/triggerable_hooks.rb
@@ -29,6 +29,12 @@ module TriggerableHooks
public_send(trigger) # rubocop:disable GitlabSecurity/PublicSend
end
+ def select_active(hooks_scope, data)
+ select do |hook|
+ ActiveHookFilter.new(hook).matches?(hooks_scope, data)
+ end
+ end
+
private
def triggerable_hooks(hooks)
diff --git a/app/models/hooks/active_hook_filter.rb b/app/models/hooks/active_hook_filter.rb
new file mode 100644
index 00000000000..ea046bea368
--- /dev/null
+++ b/app/models/hooks/active_hook_filter.rb
@@ -0,0 +1,14 @@
+class ActiveHookFilter
+ def initialize(hook)
+ @hook = hook
+ @push_events_filter_matcher = RefMatcher.new(@hook.push_events_branch_filter)
+ end
+
+ def matches?(hooks_scope, data)
+ return true if hooks_scope != :push_hooks
+ return true if @hook.push_events_branch_filter.blank?
+
+ branch_name = Gitlab::Git.branch_name(data[:ref])
+ @push_events_filter_matcher.matches?(branch_name)
+ end
+end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index f18aadefa5c..20f15c15277 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -9,6 +9,7 @@ class WebHook < ActiveRecord::Base
allow_local_network: lambda(&:allow_local_requests?) }
validates :token, format: { without: /\n/ }
+ validates :push_events_branch_filter, branch_filter: true
def execute(data, hook_name)
WebHookService.new(self, data, hook_name).execute
diff --git a/app/models/label_link.rb b/app/models/label_link.rb
index 779657b25d5..1d93a55e8e9 100644
--- a/app/models/label_link.rb
+++ b/app/models/label_link.rb
@@ -3,7 +3,7 @@
class LabelLink < ActiveRecord::Base
include Importable
- belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
+ belongs_to :target, polymorphic: true, inverse_of: :label_links # rubocop:disable Cop/PolymorphicAssociations
belongs_to :label
validates :target, presence: true, unless: :importing?
diff --git a/app/models/project.rb b/app/models/project.rb
index 435ecc59bc4..997c1cf98fc 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1186,10 +1186,9 @@ class Project < ActiveRecord::Base
def execute_hooks(data, hooks_scope = :push_hooks)
run_after_commit_or_now do
- hooks.hooks_for(hooks_scope).each do |hook|
+ hooks.hooks_for(hooks_scope).select_active(hooks_scope, data).each do |hook|
hook.async_execute(data, hooks_scope.to_s)
end
-
SystemHooksService.new.execute_hooks(data, hooks_scope)
end
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index bda1f67b8ff..f119555f16b 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -96,10 +96,10 @@ class KubernetesService < DeploymentService
# Check we can connect to the Kubernetes API
def test(*args)
- kubeclient = build_kubeclient!
+ kubeclient = build_kube_client!
- kubeclient.discover
- { success: kubeclient.discovered, result: "Checked API discovery endpoint" }
+ kubeclient.core_client.discover
+ { success: kubeclient.core_client.discovered, result: "Checked API discovery endpoint" }
rescue => err
{ success: false, result: err }
end
@@ -144,7 +144,7 @@ class KubernetesService < DeploymentService
end
def kubeclient
- @kubeclient ||= build_kubeclient!
+ @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io'])
end
def deprecated?
@@ -182,11 +182,12 @@ class KubernetesService < DeploymentService
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
- def build_kubeclient!(api_path: 'api', api_version: 'v1')
+ def build_kube_client!(api_groups: ['api'], api_version: 'v1')
raise "Incomplete settings" unless api_url && actual_namespace && token
- ::Kubeclient::Client.new(
- join_api_url(api_path),
+ Gitlab::Kubernetes::KubeClient.new(
+ api_url,
+ api_groups,
api_version,
auth_options: kubeclient_auth_options,
ssl_options: kubeclient_ssl_options,
@@ -196,7 +197,7 @@ class KubernetesService < DeploymentService
# Returns a hash of all pods in the namespace
def read_pods
- kubeclient = build_kubeclient!
+ kubeclient = build_kube_client!
kubeclient.get_pods(namespace: actual_namespace).as_json
rescue Kubeclient::HttpError => err
@@ -220,15 +221,6 @@ class KubernetesService < DeploymentService
{ bearer_token: token }
end
- def join_api_url(api_path)
- url = URI.parse(api_url)
- prefix = url.path.sub(%r{/+\z}, '')
-
- url.path = [prefix, api_path].join("/")
-
- url.to_s
- end
-
def terminal_auth
{
token: token,
diff --git a/app/models/protected_ref_matcher.rb b/app/models/protected_ref_matcher.rb
deleted file mode 100644
index bfa9180ac93..00000000000
--- a/app/models/protected_ref_matcher.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-class ProtectedRefMatcher
- def initialize(protected_ref)
- @protected_ref = protected_ref
- end
-
- # Returns all protected refs that match the given ref name.
- # This checks all records from the scope built up so far, and does
- # _not_ return a relation.
- #
- # This method optionally takes in a list of `protected_refs` to search
- # through, to avoid calling out to the database.
- def self.matching(type, ref_name, protected_refs: nil)
- (protected_refs || type.all).select { |protected_ref| protected_ref.matches?(ref_name) }
- end
-
- # Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`])
- # that match the current protected ref.
- def matching(refs)
- refs.select { |ref| @protected_ref.matches?(ref.name) }
- end
-
- # Checks if the protected ref matches the given ref name.
- def matches?(ref_name)
- return false if @protected_ref.name.blank?
-
- exact_match?(ref_name) || wildcard_match?(ref_name)
- end
-
- # Checks if this protected ref contains a wildcard
- def wildcard?
- @protected_ref.name && @protected_ref.name.include?('*')
- end
-
- protected
-
- def exact_match?(ref_name)
- @protected_ref.name == ref_name
- end
-
- def wildcard_match?(ref_name)
- return false unless wildcard?
-
- wildcard_regex === ref_name
- end
-
- def wildcard_regex
- @wildcard_regex ||= begin
- name = @protected_ref.name.gsub('*', 'STAR_DONT_ESCAPE')
- quoted_name = Regexp.quote(name)
- regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?')
- /\A#{regex_string}\z/
- end
- end
-end
diff --git a/app/models/ref_matcher.rb b/app/models/ref_matcher.rb
new file mode 100644
index 00000000000..fa7d2c0f06c
--- /dev/null
+++ b/app/models/ref_matcher.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+class RefMatcher
+ def initialize(ref_name_or_pattern)
+ @ref_name_or_pattern = ref_name_or_pattern
+ end
+
+ # Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`])
+ # that match the current protected ref.
+ def matching(refs)
+ refs.select { |ref| matches?(ref.name) }
+ end
+
+ # Checks if the protected ref matches the given ref name.
+ def matches?(ref_name)
+ return false if @ref_name_or_pattern.blank?
+
+ exact_match?(ref_name) || wildcard_match?(ref_name)
+ end
+
+ # Checks if this protected ref contains a wildcard
+ def wildcard?
+ @ref_name_or_pattern && @ref_name_or_pattern.include?('*')
+ end
+
+ protected
+
+ def exact_match?(ref_name)
+ @ref_name_or_pattern == ref_name
+ end
+
+ def wildcard_match?(ref_name)
+ return false unless wildcard?
+
+ wildcard_regex === ref_name
+ end
+
+ def wildcard_regex
+ @wildcard_regex ||= begin
+ name = @ref_name_or_pattern.gsub('*', 'STAR_DONT_ESCAPE')
+ quoted_name = Regexp.quote(name)
+ regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?')
+ /\A#{regex_string}\z/
+ end
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index cf255c8951f..929d28b9d88 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -580,7 +580,12 @@ class Repository
end
def rendered_readme
- MarkupHelper.markup_unsafe(readme.name, readme.data, project: project, markdown_engine: :redcarpet) if readme
+ return unless readme
+
+ context = { project: project }
+ context[:markdown_engine] = :redcarpet unless MarkupHelper.commonmark_for_repositories_enabled?
+
+ MarkupHelper.markup_unsafe(readme.name, readme.data, context)
end
cache_method :rendered_readme
diff --git a/app/models/user.rb b/app/models/user.rb
index f21ca1c569f..0fcc952b5cd 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -257,6 +257,7 @@ class User < ActiveRecord::Base
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
+ scope :by_username, -> (usernames) { iwhere(username: usernames) }
# Limits the users to those that have TODOs, optionally in the given state.
#
@@ -444,11 +445,11 @@ class User < ActiveRecord::Base
end
def find_by_username(username)
- iwhere(username: username).take
+ by_username(username).take
end
def find_by_username!(username)
- iwhere(username: username).take!
+ by_username(username).take!
end
def find_by_personal_access_token(token_string)
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 97e955ace36..1cd05cf3aac 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -5,7 +5,8 @@ class UserCallout < ActiveRecord::Base
enum feature_name: {
gke_cluster_integration: 1,
- gcp_signup_offer: 2
+ gcp_signup_offer: 2,
+ cluster_security_warning: 3
}
validates :user, presence: true
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index 198bb168d85..6d8b575102e 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -14,6 +14,7 @@ class IssuablePolicy < BasePolicy
rule { assignee_or_author }.policy do
enable :read_issue
enable :update_issue
+ enable :reopen_issue
enable :read_merge_request
enable :update_merge_request
end
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 94b5f37c682..a0706eaa46c 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -19,4 +19,8 @@ class IssuePolicy < IssuablePolicy
prevent :update_issue
prevent :admin_issue
end
+
+ rule { locked }.policy do
+ prevent :reopen_issue
+ end
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index fd6cc504a3b..273a93a1423 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -180,6 +180,7 @@ class ProjectPolicy < BasePolicy
enable :fork_project
enable :create_project_snippet
enable :update_issue
+ enable :reopen_issue
enable :admin_issue
enable :admin_label
enable :admin_list
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 4c2f33213d6..6a54054badc 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -11,16 +11,18 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
presents :project
+ AnchorData = Struct.new(:enabled, :label, :link, :class_modifier)
+ MAX_TAGS_TO_SHOW = 3
+
def statistics_anchors(show_auto_devops_callout:)
[
+ readme_anchor_data,
+ changelog_anchor_data,
+ contribution_guide_anchor_data,
files_anchor_data,
commits_anchor_data,
branches_anchor_data,
tags_anchor_data,
- readme_anchor_data,
- changelog_anchor_data,
- license_anchor_data,
- contribution_guide_anchor_data,
gitlab_ci_anchor_data,
autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout),
kubernetes_cluster_anchor_data
@@ -31,7 +33,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
[
readme_anchor_data,
changelog_anchor_data,
- license_anchor_data,
contribution_guide_anchor_data,
autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout),
kubernetes_cluster_anchor_data,
@@ -42,6 +43,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def empty_repo_statistics_anchors
[
+ files_anchor_data,
+ commits_anchor_data,
+ branches_anchor_data,
+ tags_anchor_data,
autodevops_anchor_data,
kubernetes_cluster_anchor_data
].compact.select { |item| item.enabled }
@@ -51,7 +56,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
[
new_file_anchor_data,
readme_anchor_data,
- license_anchor_data,
autodevops_anchor_data,
kubernetes_cluster_anchor_data
].compact.reject { |item| item.enabled }
@@ -182,95 +186,101 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def files_anchor_data
- OpenStruct.new(enabled: true,
- label: _('Files (%{human_size})') % { human_size: storage_counter(statistics.total_repository_size) },
- link: project_tree_path(project))
+ AnchorData.new(true,
+ _('Files (%{human_size})') % { human_size: storage_counter(statistics.total_repository_size) },
+ empty_repo? ? nil : project_tree_path(project))
end
def commits_anchor_data
- OpenStruct.new(enabled: true,
- label: n_('Commit (%{commit_count})', 'Commits (%{commit_count})', statistics.commit_count) % { commit_count: number_with_delimiter(statistics.commit_count) },
- link: project_commits_path(project, repository.root_ref))
+ AnchorData.new(true,
+ n_('Commit (%{commit_count})', 'Commits (%{commit_count})', statistics.commit_count) % { commit_count: number_with_delimiter(statistics.commit_count) },
+ empty_repo? ? nil : project_commits_path(project, repository.root_ref))
end
def branches_anchor_data
- OpenStruct.new(enabled: true,
- label: n_('Branch (%{branch_count})', 'Branches (%{branch_count})', repository.branch_count) % { branch_count: number_with_delimiter(repository.branch_count) },
- link: project_branches_path(project))
+ AnchorData.new(true,
+ n_('Branch (%{branch_count})', 'Branches (%{branch_count})', repository.branch_count) % { branch_count: number_with_delimiter(repository.branch_count) },
+ empty_repo? ? nil : project_branches_path(project))
end
def tags_anchor_data
- OpenStruct.new(enabled: true,
- label: n_('Tag (%{tag_count})', 'Tags (%{tag_count})', repository.tag_count) % { tag_count: number_with_delimiter(repository.tag_count) },
- link: project_tags_path(project))
+ AnchorData.new(true,
+ n_('Tag (%{tag_count})', 'Tags (%{tag_count})', repository.tag_count) % { tag_count: number_with_delimiter(repository.tag_count) },
+ empty_repo? ? nil : project_tags_path(project))
end
def new_file_anchor_data
if current_user && can_current_user_push_to_default_branch?
- OpenStruct.new(enabled: false,
- label: _('New file'),
- link: project_new_blob_path(project, default_branch || 'master'),
- class_modifier: 'new')
+ AnchorData.new(false,
+ _('New file'),
+ project_new_blob_path(project, default_branch || 'master'),
+ 'new')
end
end
def readme_anchor_data
if current_user && can_current_user_push_to_default_branch? && repository.readme.nil?
- OpenStruct.new(enabled: false,
- label: _('Add Readme'),
- link: add_readme_path)
+ AnchorData.new(false,
+ _('Add Readme'),
+ add_readme_path)
elsif repository.readme
- OpenStruct.new(enabled: true,
- label: _('Readme'),
- link: default_view != 'readme' ? readme_path : '#readme')
+ AnchorData.new(true,
+ _('Readme'),
+ default_view != 'readme' ? readme_path : '#readme')
end
end
def changelog_anchor_data
if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank?
- OpenStruct.new(enabled: false,
- label: _('Add Changelog'),
- link: add_changelog_path)
+ AnchorData.new(false,
+ _('Add Changelog'),
+ add_changelog_path)
elsif repository.changelog.present?
- OpenStruct.new(enabled: true,
- label: _('Changelog'),
- link: changelog_path)
+ AnchorData.new(true,
+ _('Changelog'),
+ changelog_path)
end
end
def license_anchor_data
- if current_user && can_current_user_push_to_default_branch? && repository.license_blob.blank?
- OpenStruct.new(enabled: false,
- label: _('Add License'),
- link: add_license_path)
- elsif repository.license_blob.present?
- OpenStruct.new(enabled: true,
- label: license_short_name,
- link: license_path)
+ if repository.license_blob.present?
+ AnchorData.new(true,
+ license_short_name,
+ license_path)
+ else
+ if current_user && can_current_user_push_to_default_branch?
+ AnchorData.new(false,
+ _('Add license'),
+ add_license_path)
+ else
+ AnchorData.new(false,
+ _('No license. All rights reserved'),
+ nil)
+ end
end
end
def contribution_guide_anchor_data
if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank?
- OpenStruct.new(enabled: false,
- label: _('Add Contribution guide'),
- link: add_contribution_guide_path)
+ AnchorData.new(false,
+ _('Add Contribution guide'),
+ add_contribution_guide_path)
elsif repository.contribution_guide.present?
- OpenStruct.new(enabled: true,
- label: _('Contribution guide'),
- link: contribution_guide_path)
+ AnchorData.new(true,
+ _('Contribution guide'),
+ contribution_guide_path)
end
end
def autodevops_anchor_data(show_auto_devops_callout: false)
if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout
- OpenStruct.new(enabled: auto_devops_enabled?,
- label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'),
- link: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
+ AnchorData.new(auto_devops_enabled?,
+ auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'),
+ project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
elsif auto_devops_enabled?
- OpenStruct.new(enabled: true,
- label: _('Auto DevOps enabled'),
- link: nil)
+ AnchorData.new(true,
+ _('Auto DevOps enabled'),
+ nil)
end
end
@@ -282,32 +292,48 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
cluster_link = new_project_cluster_path(project)
end
- OpenStruct.new(enabled: !clusters.empty?,
- label: clusters.empty? ? _('Add Kubernetes cluster') : _('Kubernetes configured'),
- link: cluster_link)
+ AnchorData.new(!clusters.empty?,
+ clusters.empty? ? _('Add Kubernetes cluster') : _('Kubernetes configured'),
+ cluster_link)
end
end
def gitlab_ci_anchor_data
if current_user && can_current_user_push_code? && repository.gitlab_ci_yml.blank? && !auto_devops_enabled?
- OpenStruct.new(enabled: false,
- label: _('Set up CI/CD'),
- link: add_ci_yml_path)
+ AnchorData.new(false,
+ _('Set up CI/CD'),
+ add_ci_yml_path)
elsif repository.gitlab_ci_yml.present?
- OpenStruct.new(enabled: true,
- label: _('CI/CD configuration'),
- link: ci_configuration_path)
+ AnchorData.new(true,
+ _('CI/CD configuration'),
+ ci_configuration_path)
end
end
def koding_anchor_data
if current_user && can_current_user_push_code? && koding_enabled? && repository.koding_yml.blank?
- OpenStruct.new(enabled: false,
- label: _('Set up Koding'),
- link: add_koding_stack_path)
+ AnchorData.new(false,
+ _('Set up Koding'),
+ add_koding_stack_path)
end
end
+ def tags_to_show
+ project.tag_list.take(MAX_TAGS_TO_SHOW)
+ end
+
+ def count_of_extra_tags_not_shown
+ if project.tag_list.count > MAX_TAGS_TO_SHOW
+ project.tag_list.count - MAX_TAGS_TO_SHOW
+ else
+ 0
+ end
+ end
+
+ def has_extra_tags?
+ count_of_extra_tags_not_shown > 0
+ end
+
private
def filename_path(filename)
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 271ff668eda..b107fc26f18 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -36,6 +36,10 @@ class BuildDetailsEntity < JobEntity
erase_project_job_path(project, build)
end
+ expose :terminal_path, if: -> (*) { can_create_build_terminal? } do |build|
+ terminal_project_job_path(project, build)
+ end
+
expose :merge_request, if: -> (*) { can?(current_user, :read_merge_request, build.merge_request) } do
expose :iid do |build|
build.merge_request.iid
@@ -69,4 +73,8 @@ class BuildDetailsEntity < JobEntity
def project
build.project
end
+
+ def can_create_build_terminal?
+ can?(current_user, :create_build_terminal, build) && build.has_terminal?
+ end
end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 025f093a428..fc7b236f7da 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -7,8 +7,8 @@ module Files
def initialize(*args)
super
- @author_email = params[:author_email]
- @author_name = params[:author_name]
+ @author_email = params[:author_email] || current_user&.email
+ @author_name = params[:author_name] || current_user&.name
@commit_message = params[:commit_message]
@last_commit_sha = params[:last_commit_sha]
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 7d60c65bb79..1259c2c2b3d 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -129,28 +129,19 @@ class IssuableBaseService < BaseService
params.merge!(command_params)
end
- def create_issuable(issuable, attributes, label_ids:)
- issuable.with_transaction_returning_status do
- if issuable.save
- issuable.update(label_ids: label_ids)
- end
- end
- end
-
def create(issuable)
handle_quick_actions_on_create(issuable)
filter_params(issuable)
params.delete(:state_event)
params[:author] ||= current_user
-
- label_ids = process_label_ids(params)
+ params[:label_ids] = issuable.label_ids.to_a + process_label_ids(params)
issuable.assign_attributes(params)
before_create(issuable)
- if params.present? && create_issuable(issuable, params, label_ids: label_ids)
+ if issuable.save
after_create(issuable)
execute_hooks(issuable)
invalidate_cache_counts(issuable, users: issuable.assignees)
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index 3bd53f9ccdc..56d59b235a7 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -3,7 +3,7 @@
module Issues
class ReopenService < Issues::BaseService
def execute(issue)
- return issue unless can?(current_user, :update_issue, issue)
+ return issue unless can?(current_user, :reopen_issue, issue)
if issue.reopen
event_service.reopen_issue(issue, current_user)
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 55750269bb4..0e76d2cc3ab 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -20,6 +20,8 @@ module MergeRequests
if merge_request.can_be_created
compare_branches
assign_title_and_description
+ assign_labels
+ assign_milestone
end
merge_request
@@ -135,6 +137,20 @@ module MergeRequests
append_closes_description
end
+ def assign_labels
+ return unless target_project.issues_enabled? && issue
+ return if merge_request.label_ids&.any?
+
+ merge_request.label_ids = issue.try(:label_ids)
+ end
+
+ def assign_milestone
+ return unless target_project.issues_enabled? && issue
+ return if merge_request.milestone_id.present?
+
+ merge_request.milestone_id = issue.try(:milestone_id)
+ end
+
def append_closes_description
return unless issue&.to_reference.present?
@@ -185,7 +201,9 @@ module MergeRequests
end
def issue
- @issue ||= target_project.get_issue(issue_iid, current_user)
+ strong_memoize(:issue) do
+ target_project.get_issue(issue_iid, current_user)
+ end
end
end
end
diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb
index fd91dc4acd0..d9a29693987 100644
--- a/app/services/merge_requests/create_from_issue_service.rb
+++ b/app/services/merge_requests/create_from_issue_service.rb
@@ -16,8 +16,6 @@ module MergeRequests
def execute
return error('Invalid issue iid') unless @issue_iid.present? && issue.present?
- params[:label_ids] = issue.label_ids if issue.label_ids.any?
-
result = CreateBranchService.new(project, current_user).execute(branch_name, ref)
return result if result[:status] == :error
@@ -58,8 +56,7 @@ module MergeRequests
source_project_id: project.id,
source_branch: branch_name,
target_project_id: project.id,
- target_branch: ref,
- milestone_id: issue.milestone_id
+ target_branch: ref
}
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 4511c500fca..50fa373025b 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -407,6 +407,12 @@ class NotificationService
end
end
+ def autodevops_disabled(pipeline, recipients)
+ recipients.each do |recipient|
+ mailer.autodevops_disabled_email(pipeline, recipient).deliver_later
+ end
+ end
+
def pages_domain_verification_succeeded(domain)
recipients_for_pages_domain(domain).each do |user|
mailer.pages_domain_verification_succeeded_email(domain, user).deliver_later
diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb
index 11b996ed4b6..de8757006f1 100644
--- a/app/services/preview_markdown_service.rb
+++ b/app/services/preview_markdown_service.rb
@@ -43,6 +43,10 @@ class PreviewMarkdownService < BaseService
end
def markdown_engine
- CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i)
+ if params[:legacy_render]
+ :redcarpet
+ else
+ CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i)
+ end
end
end
diff --git a/app/services/projects/auto_devops/disable_service.rb b/app/services/projects/auto_devops/disable_service.rb
new file mode 100644
index 00000000000..9745ab67dbd
--- /dev/null
+++ b/app/services/projects/auto_devops/disable_service.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Projects
+ module AutoDevops
+ class DisableService < BaseService
+ def execute
+ return false unless implicitly_enabled_and_first_pipeline_failure?
+
+ disable_auto_devops
+ end
+
+ private
+
+ def implicitly_enabled_and_first_pipeline_failure?
+ project.has_auto_devops_implicitly_enabled? &&
+ first_pipeline_failure?
+ end
+
+ # We're using `limit` to optimize `auto_devops pipeline` query,
+ # since we only care about the first element, and using only `.count`
+ # is an expensive operation. See
+ # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21172#note_99037378
+ # for more context.
+ def first_pipeline_failure?
+ auto_devops_pipelines.success.limit(1).count.zero? &&
+ auto_devops_pipelines.failed.limit(1).count.nonzero?
+ end
+
+ def disable_auto_devops
+ project.auto_devops_attributes = { enabled: false }
+ project.save!
+ end
+
+ def auto_devops_pipelines
+ @auto_devops_pipelines ||= project.pipelines.auto_devops_source
+ end
+ end
+ end
+end
diff --git a/app/services/wikis/create_attachment_service.rb b/app/services/wikis/create_attachment_service.rb
new file mode 100644
index 00000000000..30fe0e371a6
--- /dev/null
+++ b/app/services/wikis/create_attachment_service.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Wikis
+ class CreateAttachmentService < Files::CreateService
+ ATTACHMENT_PATH = 'uploads'.freeze
+ MAX_FILENAME_LENGTH = 255
+
+ delegate :wiki, to: :project
+ delegate :repository, to: :wiki
+
+ def initialize(*args)
+ super
+
+ @file_name = truncate_file_name(params[:file_name])
+ @file_path = File.join(ATTACHMENT_PATH, SecureRandom.hex, @file_name) if @file_name
+ @commit_message ||= "Upload attachment #{@file_name}"
+ @branch_name ||= wiki.default_branch
+ end
+
+ def create_commit!
+ commit_result(create_transformed_commit(@file_content))
+ end
+
+ private
+
+ def truncate_file_name(file_name)
+ return unless file_name.present?
+ return file_name if file_name.length <= MAX_FILENAME_LENGTH
+
+ extension = File.extname(file_name)
+ truncate_at = MAX_FILENAME_LENGTH - extension.length - 1
+ base_name = File.basename(file_name, extension)[0..truncate_at]
+ base_name + extension
+ end
+
+ def validate!
+ validate_file_name!
+ validate_permissions!
+ end
+
+ def validate_file_name!
+ raise_error('The file name cannot be empty') unless @file_name
+ end
+
+ def validate_permissions!
+ unless can?(current_user, :create_wiki, project)
+ raise_error('You are not allowed to push to the wiki')
+ end
+ end
+
+ def create_transformed_commit(content)
+ repository.create_file(
+ current_user,
+ @file_path,
+ content,
+ message: @commit_message,
+ branch_name: @branch_name,
+ author_email: @author_email,
+ author_name: @author_name)
+ end
+
+ def commit_result(commit_id)
+ {
+ file_name: @file_name,
+ file_path: @file_path,
+ branch: @branch_name,
+ commit: commit_id
+ }
+ end
+ end
+end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index b1365659834..ffc1e5f75ca 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -122,12 +122,6 @@ class FileUploader < GitlabUploader
}
end
- def markdown_link
- markdown = +"[#{markdown_name}](#{secure_url})"
- markdown.prepend("!") if image_or_video? || dangerous?
- markdown
- end
-
def to_h
{
alt: markdown_name,
@@ -192,10 +186,6 @@ class FileUploader < GitlabUploader
storage.delete_dir!(store_dir) # only remove when empty
end
- def markdown_name
- (image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]")
- end
-
def identifier
@identifier ||= filename
end
diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb
index f6af023e0f9..557b13a8bd6 100644
--- a/app/uploaders/job_artifact_uploader.rb
+++ b/app/uploaders/job_artifact_uploader.rb
@@ -5,6 +5,7 @@ class JobArtifactUploader < GitlabUploader
include ObjectStorage::Concern
ObjectNotReadyError = Class.new(StandardError)
+ UnknownFileLocationError = Class.new(StandardError)
storage_options Gitlab.config.artifacts
@@ -23,10 +24,22 @@ class JobArtifactUploader < GitlabUploader
def dynamic_segment
raise ObjectNotReadyError, 'JobArtifact is not ready' unless model.id
- creation_date = model.created_at.utc.strftime('%Y_%m_%d')
+ if model.hashed_path?
+ hashed_path
+ elsif model.legacy_path?
+ legacy_path
+ else
+ raise UnknownFileLocationError
+ end
+ end
+ def hashed_path
File.join(disk_hash[0..1], disk_hash[2..3], disk_hash,
- creation_date, model.job_id.to_s, model.id.to_s)
+ model.created_at.utc.strftime('%Y_%m_%d'), model.job_id.to_s, model.id.to_s)
+ end
+
+ def legacy_path
+ File.join(model.created_at.utc.strftime('%Y_%m'), model.project_id.to_s, model.job_id.to_s)
end
def disk_hash
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
index 2a2b54a9270..e8a2dce7755 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -2,32 +2,7 @@
# Extra methods for uploader
module UploaderHelper
- IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze
- # We recommend using the .mp4 format over .mov. Videos in .mov format can
- # still be used but you really need to make sure they are served with the
- # proper MIME type video/mp4 and not video/quicktime or your videos won't play
- # on IE >= 9.
- # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
- VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze
- # These extension types can contain dangerous code and should only be embedded inline with
- # proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline".
- DANGEROUS_EXT = %w[svg].freeze
-
- def image?
- extension_match?(IMAGE_EXT)
- end
-
- def video?
- extension_match?(VIDEO_EXT)
- end
-
- def image_or_video?
- image? || video?
- end
-
- def dangerous?
- extension_match?(DANGEROUS_EXT)
- end
+ include Gitlab::FileMarkdownLinkBuilder
private
diff --git a/app/validators/branch_filter_validator.rb b/app/validators/branch_filter_validator.rb
new file mode 100644
index 00000000000..ef482aaaa63
--- /dev/null
+++ b/app/validators/branch_filter_validator.rb
@@ -0,0 +1,35 @@
+# BranchFilterValidator
+#
+# Custom validator for branch names. Squishes whitespace and ignores empty
+# string. This only checks that a string is a valid git branch name. It does
+# not check whether a branch already exists.
+#
+# Example:
+#
+# class Webhook < ActiveRecord::Base
+# validates :push_events_branch_filter, branch_name: true
+# end
+#
+class BranchFilterValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ value.squish! unless value.nil?
+
+ if value.present?
+ value_without_wildcards = value.tr('*', 'x')
+
+ unless Gitlab::GitRefValidator.validate(value_without_wildcards)
+ record.errors[attribute] << "is not a valid branch name"
+ end
+
+ unless value.length <= 4000
+ record.errors[attribute] << "is longer than the allowed length of 4000 characters."
+ end
+ end
+ end
+
+ private
+
+ def contains_wildcard?(value)
+ value.include?('*')
+ end
+end
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index 2495defb6a7..788595877ea 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -2,7 +2,7 @@
= form_errors(@application_setting)
%fieldset
- .form-group
+ .form-group.mb-2
.form-check
= f.check_box :version_check_enabled, class: 'form-check-input'
= f.label :version_check_enabled, class: 'form-check-label' do
@@ -16,23 +16,26 @@
.form-check
= f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input'
= f.label :usage_ping_enabled, class: 'form-check-label' do
- Enable usage ping
+ = _('Enable usage ping')
.form-text.text-muted
- if can_be_configured
- To help improve GitLab and its user experience, GitLab will
- periodically collect usage information.
- = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
- about what information is shared with GitLab Inc. Visit
- = link_to _('Cohorts'), instance_statistics_cohorts_path(anchor: 'usage-ping')
- to see the JSON payload sent.
+ %p.mb-2= _('To help improve GitLab and its user experience, GitLab will periodically collect usage information.')
+
+ - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
+ - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
+ %p.mb-2= s_('%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
+
+ %button.btn.js-usage-ping-payload-trigger{ type: 'button' }
+ .js-spinner.d-none= icon('spinner spin')
+ .js-text.d-inline= _('Preview payload')
+ %pre.usage-data.js-usage-ping-payload.js-syntax-highlight.code.highlight.mt-2.d-none{ data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
- else
- The usage ping is disabled, and cannot be configured through this
- form. For more information, see the documentation on
- = succeed '.' do
- = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
- .form-group
+ = _('The usage ping is disabled, and cannot be configured through this form.')
+ - deactivating_usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
+ - deactivating_usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_usage_ping_path }
+ = s_('For more information, see the documentation on %{deactivating_usage_ping_link_start}deactivating the usage ping%{deactivating_usage_ping_link_end}.').html_safe % { deactivating_usage_ping_link_start: deactivating_usage_ping_link_start, deactivating_usage_ping_link_end: '</a>'.html_safe }
+ .form-group.mt-3
= f.label :instance_statistics_visibility_private, _('Instance Statistics visibility')
= f.select :instance_statistics_visibility_private, options_for_select({_('All users') => false, _('Only admins') => true}, Gitlab::CurrentSettings.instance_statistics_visibility_private?), {}, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
-
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index 6133a7646f4..194a8157013 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -325,6 +325,14 @@
.settings-content
= render partial: 'repository_mirrors_form'
+= render_if_exists 'admin/application_settings/geo', expanded: expanded
+
+= render_if_exists 'admin/application_settings/external_authorization_service_form', expanded: expanded
+
+= render_if_exists 'admin/application_settings/elasticsearch_form', expanded: expanded
+
+= render_if_exists 'admin/application_settings/slack', expanded: expanded
+
= render_if_exists 'admin/application_settings/templates', expanded: expanded
%section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) }
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 0ee563ac066..17a9c8df872 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -1,9 +1,9 @@
= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f|
.form-group
- = f.label "Username or email", for: "user_login"
+ = f.label "Username or email", for: "user_login", class: 'label-bold'
= f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required."
.form-group
- = f.label :password
+ = f.label :password, class: 'label-bold'
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required."
- if devise_mapping.rememberable?
.remember-me
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index 3723814debe..269a3721e06 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -1,14 +1,17 @@
-.omniauth-container
- %p
- %span.light
- Sign in with &nbsp;
- - providers = enabled_button_based_providers
+.omniauth-container.prepend-top-15
+ %label.label-bold.d-block
+ Sign in with
+ - providers = enabled_button_based_providers
+ .d-flex.justify-content-between.flex-wrap
- providers.each do |provider|
- %span.light
- - has_icon = provider_has_icon?(provider)
- = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}"
- %fieldset.prepend-top-10.remember-me
- %label
- = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
+ - has_icon = provider_has_icon?(provider)
+ = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'btn d-flex align-items-center omniauth-btn text-left oauth-login', id: "oauth-login-#{provider}" do
+ - if has_icon
+ = provider_image_tag(provider)
%span
- Remember me
+ = label_for_provider(provider)
+ %fieldset.remember-me
+ %label
+ = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
+ %span
+ Remember me
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index ee7369f54a9..90ed20404c5 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -4,24 +4,24 @@
.devise-errors
= devise_error_messages!
.form-group
- = f.label :name, 'Full name'
+ = f.label :name, 'Full name', class: 'label-bold'
= f.text_field :name, class: "form-control top", required: true, title: "This field is required."
.username.form-group
- = f.label :username
+ = f.label :username, class: 'label-bold'
= f.text_field :username, class: "form-control middle", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: 'Please create a username with only alphanumeric characters.'
%p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability...
.form-group
- = f.label :email
+ = f.label :email, class: 'label-bold'
= f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address."
.form-group
- = f.label :email_confirmation
+ = f.label :email_confirmation, class: 'label-bold'
= f.email_field :email_confirmation, class: "form-control middle", required: true, title: "Please retype the email address."
.form-group.append-bottom-20#password-strength
- = f.label :password
+ = f.label :password, class: 'label-bold'
= f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
- %p.gl-field-hint Minimum length is #{@minimum_password_length} characters
+ %p.gl-field-hint.text-secondary Minimum length is #{@minimum_password_length} characters
- if Gitlab::CurrentSettings.current_application_settings.enforce_terms?
.form-group
= check_box_tag :terms_opt_in, '1', false, required: true
@@ -34,8 +34,3 @@
= recaptcha_tags
.submit-container
= f.submit "Register", class: "btn-register btn"
-.clearfix.submit-container
- %p
- %span.light Didn't receive a confirmation email?
- = succeed '.' do
- = link_to "Request a new one", new_confirmation_path(:user)
diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml
index 4b6c4581eb3..6b8dd156874 100644
--- a/app/views/discussions/_diff_discussion.html.haml
+++ b/app/views/discussions/_diff_discussion.html.haml
@@ -4,7 +4,6 @@
-# Text diff discussions
- expanded = local_assigns.fetch(:expanded, true)
%tr.notes_holder{ class: ('hide' unless expanded) }
- %td.notes_line{ colspan: 2 }
- %td.notes_content
+ %td.notes_content{ colspan: 3 }
.content{ class: ('hide' unless expanded) }
= render partial: "discussions/notes", collection: discussions, as: :discussion, locals: { disable_collapse_class: true }
diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml
index 079d9083dff..2e621c4082d 100644
--- a/app/views/discussions/_parallel_diff_discussion.html.haml
+++ b/app/views/discussions/_parallel_diff_discussion.html.haml
@@ -1,21 +1,17 @@
- expanded = [*discussions_left, *discussions_right].any?(&:expanded?)
%tr.notes_holder{ class: ('hide' unless expanded) }
- if discussions_left
- %td.notes_line.old
- %td.notes_content.parallel.old
+ %td.notes_content.parallel.old{ colspan: 2 }
.content{ class: ('hide' unless discussions_left.any?(&:expanded?)) }
= render partial: "discussions/notes", collection: discussions_left, as: :discussion, line_type: 'old', locals: { disable_collapse_class: true }
- else
- %td.notes_line.old= ("")
- %td.notes_content.parallel.old
+ %td.notes_content.parallel.old{ colspan: 2 }
.content
- if discussions_right
- %td.notes_line.new
- %td.notes_content.parallel.new
+ %td.notes_content.parallel.new{ colspan: 2 }
.content{ class: ('hide' unless discussions_right.any?(&:expanded?)) }
= render partial: "discussions/notes", collection: discussions_right, as: :discussion, line_type: 'new', locals: { disable_collapse_class: true }
- else
- %td.notes_line.new= ("")
- %td.notes_content.parallel.new
+ %td.notes_content.parallel.new{ colspan: 2 }
.content
diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml
index bc1d32607e4..c5b033b1185 100644
--- a/app/views/events/_event_push.atom.haml
+++ b/app/views/events/_event_push.atom.haml
@@ -1,7 +1,7 @@
%div{ xmlns: "http://www.w3.org/1999/xhtml" }
%p
%strong= event.author_name
- = link_to "(#{truncate_sha(event.commit_id)})", project_commit_path(event.project, event.commit_id)
+ = link_to "(#{truncate_sha(event.commit_id)})", event_feed_url(event)
%i
at
= event.created_at.to_s(:short)
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index e1e38a7e82f..e6821009d03 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -1,28 +1,38 @@
- @no_container = true
- page_title "Labels"
- can_admin_label = can?(current_user, :admin_label, @group)
-- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
- issuables = ['issues', 'merge requests']
+- search = params[:search]
- if can_admin_label
- content_for(:header_content) do
.nav-controls
= link_to _('New label'), new_group_label_path(@group), class: "btn btn-new"
-- if @labels.exists?
+- if @labels.exists? || search.present?
#promote-label-modal
%div{ class: container_class }
.top-area.adjust
.nav-text
= _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence }
+ .nav-controls
+ = form_tag group_labels_path(@group), method: :get do
+ .input-group
+ = search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false }
+ %span.input-group-append
+ %button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') }
+ = icon("search")
.labels-container.prepend-top-5
- .other-labels
- - if can_admin_label
- %h5{ class: ('hide' if hide) } Labels
- %ul.content-list.manage-labels-list.js-other-labels
- = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false }
- = paginate @labels, theme: 'gitlab'
+ - if @labels.any?
+ .other-labels
+ %h5= _('Labels')
+ %ul.content-list.manage-labels-list.js-other-labels
+ = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false }
+ = paginate @labels, theme: 'gitlab'
+ - elsif search.present?
+ .nothing-here-block
+ = _('No labels with such name or description')
- else
= render 'shared/empty_states/labels'
diff --git a/app/views/instance_statistics/cohorts/index.html.haml b/app/views/instance_statistics/cohorts/index.html.haml
index 5e9a8c083af..e135bab10d8 100644
--- a/app/views/instance_statistics/cohorts/index.html.haml
+++ b/app/views/instance_statistics/cohorts/index.html.haml
@@ -1,16 +1,16 @@
-- breadcrumb_title "Cohorts"
+- breadcrumb_title _("Cohorts")
- @no_container = true
%div{ class: container_class }
- if @cohorts
= render 'cohorts_table'
- = render 'usage_ping'
- else
.bs-callout.bs-callout-warning.clearfix
%p
- User cohorts are only shown when the
- = link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping'), target: '_blank'
- is enabled. To enable it and see user cohorts,
- visit
- = succeed '.' do
- = link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics')
+ - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
+ - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
+ = s_('User Cohorts are only shown when the %{usage_ping_link_start}usage ping%{usage_ping_link_end} is enabled.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
+ - if current_user.admin?
+ - application_settings_path = admin_application_settings_path(anchor: 'usage-statistics')
+ - application_settings_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: application_settings_path }
+ = s_('To enable it and see User Cohorts, visit %{application_settings_link_start}application settings%{application_settings_link_end}.').html_safe % { application_settings_link_start: application_settings_link_start, application_settings_link_end: '</a>'.html_safe }
diff --git a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml b/app/views/instance_statistics/conversational_development_index/_disabled.html.haml
index 0a741b50960..0a5717f75e1 100644
--- a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml
+++ b/app/views/instance_statistics/conversational_development_index/_disabled.html.haml
@@ -1,9 +1,14 @@
.container.convdev-empty
.col-sm-12.justify-content-center.text-center
= custom_icon('convdev_no_index')
- %h4 Usage ping is not enabled
- %p
- ConvDev is only shown when the
- = link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics'), target: '_blank'
- is enabled. Enable usage ping to get an overview of how you are using GitLab from a feature perspective
- = link_to 'Enable usage ping', admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary'
+ %h4= _('Usage ping is not enabled')
+ - if !current_user.admin?
+ %p
+ - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
+ - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
+ = s_('In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
+ - if current_user.admin?
+ %p
+ = _('Enable usage ping to get an overview of how you are using GitLab from a feature perspective.')
+ - if current_user.admin?
+ = link_to _('Enable usage ping'), admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary'
diff --git a/app/views/instance_statistics/conversational_development_index/index.html.haml b/app/views/instance_statistics/conversational_development_index/index.html.haml
index dd63b98376f..1e7db4982d6 100644
--- a/app/views/instance_statistics/conversational_development_index/index.html.haml
+++ b/app/views/instance_statistics/conversational_development_index/index.html.haml
@@ -1,12 +1,13 @@
- @no_container = true
-- page_title 'ConvDev Index'
+- page_title _('ConvDev Index')
+- usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
.container
- - if show_callout?('convdev_intro_callout_dismissed')
+ - if usage_ping_enabled && show_callout?('convdev_intro_callout_dismissed')
= render 'callout'
.prepend-top-default
- - if !Gitlab::CurrentSettings.usage_ping_enabled
+ - if !usage_ping_enabled
= render 'disabled'
- elsif @metric.blank?
= render 'no_data'
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 0ca34b276a7..1f4d24d996c 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -4,7 +4,7 @@
%body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
= render "layouts/init_auto_complete" if @gfm_form
= render 'peek/bar'
- = render "layouts/header/default"
+ = render partial: "layouts/header/default", locals: { project: @project, group: @group }
= render 'layouts/page', sidebar: sidebar, nav: nav
= yield :scripts_body
diff --git a/app/views/layouts/fullscreen.html.haml b/app/views/layouts/fullscreen.html.haml
index 95db8313821..e29f646ed4f 100644
--- a/app/views/layouts/fullscreen.html.haml
+++ b/app/views/layouts/fullscreen.html.haml
@@ -3,7 +3,7 @@
= render "layouts/head"
%body{ class: "#{user_application_theme} #{@body_class} fullscreen-layout", data: { page: body_data_page } }
= render 'peek/bar'
- = render "layouts/header/default"
+ = render partial: "layouts/header/default", locals: { project: @project, group: @group }
= render 'shared/outdated_browser'
.mobile-overlay
.alert-wrapper
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index e8d31992149..044b49c12cc 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,3 +1,10 @@
+- if project
+ - search_path_url = search_path(project_id: project.id)
+- elsif group
+ - search_path_url = search_path(group_id: group.id)
+- else
+ - search_path_url = search_path
+
%header.navbar.navbar-gitlab.qa-navbar.navbar-expand-sm
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
@@ -24,26 +31,25 @@
%li.nav-item.d-none.d-sm-none.d-md-block.m-auto
= render 'layouts/search' unless current_controller?(:search)
%li.nav-item.d-inline-block.d-sm-none.d-md-none
- = link_to search_path, title: _('Search'), aria: { label: _("Search") }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = link_to search_path_url, title: _('Search'), aria: { label: _('Search') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('search', size: 16)
-
- if header_link?(:issues)
= nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do
- = link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _("Issues") }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _('Issues') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('issues', size: 16)
- issues_count = assigned_issuables_count(:issues)
%span.badge.badge-pill.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
- if header_link?(:merge_requests)
= nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do
- = link_to assigned_mrs_dashboard_path, title: _('Merge requests'), class: 'dashboard-shortcuts-merge_requests', aria: { label: _("Merge requests") }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = link_to assigned_mrs_dashboard_path, title: _('Merge requests'), class: 'dashboard-shortcuts-merge_requests', aria: { label: _('Merge requests') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('git-merge', size: 16)
- merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.badge-pill.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count)
- if header_link?(:todos)
= nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do
- = link_to dashboard_todos_path, title: _('Todos'), aria: { label: _("Todos") }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = link_to dashboard_todos_path, title: _('Todos'), aria: { label: _('Todos') }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('todo-done', size: 16)
%span.badge.badge-pill.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count)
@@ -56,7 +62,7 @@
= render 'layouts/header/current_user_dropdown'
- if header_link?(:admin_impersonation)
%li.nav-item.impersonation
- = link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _("Stop impersonation"), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _('Stop impersonation'), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('user-secret')
- if header_link?(:sign_in)
%li.nav-item
@@ -64,8 +70,7 @@
- sign_in_text = allow_signup? ? _('Sign in / Register') : _('Sign in')
= link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
-
%button.navbar-toggler.d-block.d-sm-none{ type: 'button' }
- %span.sr-only= _("Toggle navigation")
+ %span.sr-only= _('Toggle navigation')
= sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
diff --git a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml b/app/views/layouts/nav/sidebar/_instance_statistics.html.haml
index b8ff448f261..57180f27146 100644
--- a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml
+++ b/app/views/layouts/nav/sidebar/_instance_statistics.html.haml
@@ -18,16 +18,17 @@
%strong.fly-out-top-item-name
= _('ConvDev Index')
- = nav_link(controller: :cohorts) do
- = link_to instance_statistics_cohorts_path do
- .nav-icon-container
- = sprite_icon('users')
- %span.nav-item-name
- = _('Cohorts')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do
- = link_to instance_statistics_cohorts_path do
- %strong.fly-out-top-item-name
- = _('Cohorts')
+ - if Gitlab::CurrentSettings.usage_ping_enabled
+ = nav_link(controller: :cohorts) do
+ = link_to instance_statistics_cohorts_path do
+ .nav-icon-container
+ = sprite_icon('users')
+ %span.nav-item-name
+ = _('Cohorts')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do
+ = link_to instance_statistics_cohorts_path do
+ %strong.fly-out-top-item-name
+ = _('Cohorts')
= render 'shared/sidebar_toggle_button'
diff --git a/app/views/notify/_failed_builds.html.haml b/app/views/notify/_failed_builds.html.haml
new file mode 100644
index 00000000000..7c563bb016c
--- /dev/null
+++ b/app/views/notify/_failed_builds.html.haml
@@ -0,0 +1,32 @@
+%tr
+ %td{ colspan: 2, style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.4; padding: 0 8px 16px; text-align: center;" }
+ had
+ = failed.size
+ failed
+ #{'build'.pluralize(failed.size)}.
+%tr.table-warning
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; border: 1px solid #ededed; border-bottom: 0; border-radius: 4px 4px 0 0; overflow: hidden; background-color: #fdf4f6; color: #d22852; font-size: 14px; line-height: 1.4; text-align: center; padding: 8px 16px;" }
+ Logs may contain sensitive data. Please consider before forwarding this email.
+%tr.section
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 16px; border: 1px solid #ededed; border-radius: 4px; overflow: hidden; border-top: 0; border-radius: 0 0 4px 4px;" }
+ %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width: 100%; border-collapse: collapse;" }
+ %tbody
+ - failed.each do |build|
+ %tr.build-state
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 16px 0; color: #8c8c8c; font-weight: 500; font-size: 14px;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse: collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #d22f57; font-weight: 500; font-size: 16px; vertical-align: middle; padding-right: 8px; line-height: 10px" }
+ %img{ alt: "✖", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display: block;", width: "10" }/
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #8c8c8c; font-weight: 500; font-size: 14px; vertical-align: middle;" }
+ = build.stage
+ %td{ align: "right", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 16px 0; color: #8c8c8c; font-weight: 500; font-size: 14px;" }
+ = render "notify/links/#{build.to_partial_path}", pipeline: pipeline, build: build
+ %tr.build-log
+ - if build.has_trace?
+ %td{ colspan: "2", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 0 16px;" }
+ %pre{ style: "font-family: Monaco,'Lucida Console','Courier New',Courier,monospace; background-color: #fafafa; border-radius: 4px; overflow: hidden; white-space: pre-wrap; word-break: break-all; font-size:13px; line-height: 1.4; padding: 16px 8px; color: #333333; margin: 0;" }
+ = build.trace.html(last_lines: 10).html_safe
+ - else
+ %td{ colspan: "2" }
diff --git a/app/views/notify/autodevops_disabled_email.html.haml b/app/views/notify/autodevops_disabled_email.html.haml
new file mode 100644
index 00000000000..65a2f75a3e2
--- /dev/null
+++ b/app/views/notify/autodevops_disabled_email.html.haml
@@ -0,0 +1,49 @@
+%tr.alert
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 8px 16px; border-radius: 4px; font-size: 14px; line-height: 1.3; text-align: center; overflow: hidden; background-color: #d22f57; color: #ffffff;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse: collapse; margin: 0 auto;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; vertical-align: middle; color: #ffffff; text-align: center;" }
+ Auto DevOps pipeline was disabled for #{@project.name}
+
+%tr.pre-section
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.7; padding: 16px 8px 0;" }
+ The Auto DevOps pipeline failed for pipeline
+ %a{ href: pipeline_url(@pipeline), style: "color: #1b69b6; text-decoration:none;" }
+ = "\##{@pipeline.iid}"
+ and has been disabled for
+ %a{ href: project_url(@project), style: "color: #1b69b6; text-decoration: none;" }
+ = @project.name + "."
+ In order to use the Auto DevOps pipeline with your project, please review the
+ %a{ href: 'https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages', style: "color:#1b69b6;text-decoration:none;" } currently supported languages,
+ adjust your project accordingly, and turn on the Auto DevOps pipeline within your
+ %a{ href: project_settings_ci_cd_url(@project), style: "color: #1b69b6; text-decoration: none;" }
+ CI/CD project settings.
+
+%tr.pre-section
+ %td{ style: 'text-align: center;border-bottom:1px solid #ededed' }
+ %a{ href: 'https://docs.gitlab.com/ee/topics/autodevops/', style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %button{ type: 'button', style: 'border-color: #dfdfdf; border-style: solid; border-width: 1px; border-radius: 4px; font-size: 14px; padding: 8px 16px; background-color:#fff; margin: 8px 0; cursor: pointer;' }
+ Learn more about Auto DevOps
+
+%tr.pre-section
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.4; padding: 16px 8px; text-align: center;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
+ %tbody
+ %tr
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size:14px; font-weight:500;line-height: 1.4; vertical-align: baseline;" }
+ Pipeline
+ %a{ href: pipeline_url(@pipeline), style: "color: #1b69b6; text-decoration: none;" }
+ = "\##{@pipeline.id}"
+ triggered by
+ - if @pipeline.user
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 15px; line-height: 1.4; vertical-align: middle; padding-right: 8px; padding-left:8px", width: "24" }
+ %img.avatar{ height: "24", src: avatar_icon_for_user(@pipeline.user, 24, only_path: false), style: "display: block; border-radius: 12px; margin: -2px 0;", width: "24", alt: "" }/
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 14px; font-weight: 500; line-height: 1.4; vertical-align: baseline;" }
+ %a.muted{ href: user_url(@pipeline.user), style: "color: #333333; text-decoration: none;" }
+ = @pipeline.user.name
+ - else
+ %td{ style: "font-family: 'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace; font-size: 14px; line-height: 1.4; vertical-align: baseline; padding:0 8px;" }
+ API
+
+= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed
diff --git a/app/views/notify/autodevops_disabled_email.text.erb b/app/views/notify/autodevops_disabled_email.text.erb
new file mode 100644
index 00000000000..695780c3145
--- /dev/null
+++ b/app/views/notify/autodevops_disabled_email.text.erb
@@ -0,0 +1,20 @@
+Auto DevOps pipeline was disabled for <%= @project.name %>
+
+The Auto DevOps pipeline failed for pipeline <%= @pipeline.iid %> (<%= pipeline_url(@pipeline) %>) and has been disabled for <%= @project.name %>. In order to use the Auto DevOps pipeline with your project, please review the currently supported languagues (https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages), adjust your project accordingly, and turn on the Auto DevOps pipeline within your CI/CD project settings (<%= project_settings_ci_cd_url(@project) %>).
+
+<% if @pipeline.user -%>
+ Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
+<% else -%>
+ Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
+<% end -%>
+<% failed = @pipeline.statuses.latest.failed -%>
+had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
+
+<% failed.each do |build| -%>
+ <%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %>
+ Stage: <%= build.stage %>
+ Name: <%= build.name %>
+ <% if build.has_trace? -%>
+ Trace: <%= build.trace.raw(last_lines: 10) %>
+ <% end -%>
+<% end -%>
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index baafaa6e3a0..86dcca4a447 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -107,36 +107,5 @@
- else
%td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" }
API
-- failed = @pipeline.statuses.latest.failed
-%tr
- %td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" }
- had
- = failed.size
- failed
- #{'build'.pluralize(failed.size)}.
-%tr.table-warning
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" }
- Logs may contain sensitive data. Please consider before forwarding this email.
-%tr.section
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" }
- %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" }
- %tbody
- - failed.each do |build|
- %tr.build-state
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#d22f57;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;line-height:10px" }
- %img{ alt: "✖", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" }
- = build.stage
- %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
- = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build
- %tr.build-log
- - if build.has_trace?
- %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" }
- %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" }
- = build.trace.html(last_lines: 10).html_safe
- - else
- %td{ colspan: "2" }
+
+= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 6f08a294c5d..9f79feb4ddd 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -77,7 +77,7 @@
= f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9' },
help: "Your name was automatically set based on your #{ attribute_provider_label(:name) } account, so people you know can recognize you."
- else
- = f.text_field :name, required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you."
+ = f.text_field :name, label: 'Full name', required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you."
= f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' }
- if @user.read_only_attribute?(:email)
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 1b6c4193c4d..ced6a2a0399 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,16 +1,35 @@
- empty_repo = @project.empty_repo?
-.project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
+- license = @project.license_anchor_data
+.project-home-panel{ class: ("empty-project" if empty_repo) }
.limit-container-width{ class: container_class }
- .avatar-container.s70.project-avatar
- = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile', width: 70, height: 70)
- %h1.project-title.qa-project-name
- = @project.name
- %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
- = visibility_level_icon(@project.visibility_level, fw: false)
+ .project-header.d-flex.flex-row.flex-wrap.align-items-center.append-bottom-8
+ .project-title-row.d-flex.align-items-center
+ .avatar-container.project-avatar.float-none
+ = project_icon(@project, alt: @project.name, class: 'avatar avatar-tile')
+ %h1.project-title.d-flex.align-items-baseline.qa-project-name
+ = @project.name
+ .project-metadata.d-flex.flex-row.flex-wrap.align-items-baseline
+ .project-visibility.d-inline-flex.align-items-baseline.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
+ = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'})
+ = visibility_level_label(@project.visibility_level)
+ - if license.present?
+ .project-license.d-inline-flex.align-items-baseline
+ = link_to_if license.link, sprite_icon('scale', size: 16, css_class: 'icon') + license.label, license.link, class: license.enabled ? 'btn btn-link btn-secondary-hover-link' : 'btn btn-link'
+ - if @project.tag_list.present?
+ .project-tag-list.d-inline-flex.align-items-baseline.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_tags? ? @project.tag_list.join(', ') : nil }
+ = sprite_icon('tag', size: 16, css_class: 'icon')
+ = @project.tags_to_show
+ - if @project.has_extra_tags?
+ = _("+ %{count} more") % { count: @project.count_of_extra_tags_not_shown }
.project-home-desc
- if @project.description.present?
- = markdown_field(@project, :description)
+ .project-description
+ .project-description-markdown.read-more-container
+ = markdown_field(@project, :description)
+ %button.btn.btn-blank.btn-link.text-secondary.js-read-more-trigger.text-secondary.d-lg-none{ type: "button" }
+ = _("Read more")
+
- if can?(current_user, :read_project, @project)
.text-secondary.prepend-top-8
= s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id }
@@ -25,34 +44,42 @@
- deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
= deleted_message % { project_name: fork_source_name(@project) }
- .project-badges.prepend-top-default.append-bottom-default
- - @project.badges.each do |badge|
- %a.append-right-8{ href: badge.rendered_link_url(@project),
- target: '_blank',
- rel: 'noopener noreferrer' }>
- %img.project-badge{ src: badge.rendered_image_url(@project),
- 'aria-hidden': true,
- alt: '' }>
-
- .project-repo-buttons
- .count-buttons
+ - if @project.badges.present?
+ .project-badges.prepend-top-default.append-bottom-default
+ - @project.badges.each do |badge|
+ %a.append-right-8{ href: badge.rendered_link_url(@project),
+ target: '_blank',
+ rel: 'noopener noreferrer' }>
+ %img.project-badge{ src: badge.rendered_image_url(@project),
+ 'aria-hidden': true,
+ alt: 'Project badge' }>
+
+ .project-repo-buttons.d-inline-flex.flex-wrap
+ .count-buttons.d-inline-flex
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
- %span.d-none.d-sm-inline
- - if can?(current_user, :download_code, @project)
- .project-clone-holder
- = render "shared/clone_panel"
+ - if can?(current_user, :download_code, @project)
+ .project-clone-holder.d-inline-flex.d-sm-none
+ = render "shared/mobile_clone_panel"
- - if show_xcode_link?(@project)
- .project-action-button.project-xcode.inline
- = render "projects/buttons/xcode_link"
+ .project-clone-holder.d-none.d-sm-inline-flex
+ = render "shared/clone_panel"
- - if current_user
- - if can?(current_user, :download_code, @project)
+ - if show_xcode_link?(@project)
+ .project-action-button.project-xcode.inline
+ = render "projects/buttons/xcode_link"
+
+ - if current_user
+ - if can?(current_user, :download_code, @project)
+ .d-none.d-sm-inline-flex
= render 'projects/buttons/download', project: @project, ref: @ref
+ .d-none.d-sm-inline-flex
= render 'projects/buttons/dropdown'
+ .d-none.d-sm-inline-flex
= render 'projects/buttons/koding'
+ .d-none.d-sm-inline-flex
= render 'shared/notifications/button', notification_setting: @notification_setting
+ .d-none.d-sm-inline-flex
= render 'shared/members/access_request_buttons', source: @project
diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml
index 15ec58289e3..4cf49f3cf62 100644
--- a/app/views/projects/_stat_anchor_list.html.haml
+++ b/app/views/projects/_stat_anchor_list.html.haml
@@ -1,7 +1,7 @@
- anchors = local_assigns.fetch(:anchors, [])
- return unless anchors.any?
-%ul.nav.justify-content-center
+%ul.nav
- anchors.each do |anchor|
%li.nav-item
= link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do
diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml
index 5646dc464f8..5adca007f7e 100644
--- a/app/views/projects/_wiki.html.haml
+++ b/app/views/projects/_wiki.html.haml
@@ -2,7 +2,7 @@
%div{ class: container_class }
.prepend-top-default.append-bottom-default
.wiki
- = render_wiki_content(@wiki_home)
+ = render_wiki_content(@wiki_home, legacy_render_context(params))
- else
- can_create_wiki = can?(current_user, :create_wiki, @project)
.project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] }
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index a4b1b496b69..cf273aab108 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -5,6 +5,7 @@
%ul.blob-commit-info
= render 'projects/commits/commit', commit: @last_commit, project: @project, ref: @ref
+ = render_if_exists 'projects/blob/owners', blob: blob
= render "projects/blob/auxiliary_viewer", blob: blob
#blob-content-holder.blob-content-holder
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 27cf040da7c..fdab8a53b41 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -21,7 +21,7 @@
Write
%li
- = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id) do
+ = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id, legacy_render: params[:legacy_render]) do
= editing_preview_title(@blob.name)
= form_tag(project_update_blob_path(@project, @id), method: :put, class: 'js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do
diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml
index da2cef17e8a..eb65cd90ea8 100644
--- a/app/views/projects/blob/preview.html.haml
+++ b/app/views/projects/blob/preview.html.haml
@@ -2,7 +2,7 @@
.diff-content
- if markup?(@blob.name)
.file-content.wiki
- = markup(@blob.name, @content)
+ = markup(@blob.name, @content, legacy_render_context(params))
- else
.file-content.code.js-syntax-highlight
- unless @diff_lines.empty?
diff --git a/app/views/projects/blob/viewers/_markup.html.haml b/app/views/projects/blob/viewers/_markup.html.haml
index 230305b488d..bd12cadf240 100644
--- a/app/views/projects/blob/viewers/_markup.html.haml
+++ b/app/views/projects/blob/viewers/_markup.html.haml
@@ -1,4 +1,6 @@
- blob = viewer.blob
-- rendered_markup = blob.rendered_markup if blob.respond_to?(:rendered_markup)
+- context = legacy_render_context(params)
+- unless context[:markdown_engine] == :redcarpet
+ - context[:rendered] = blob.rendered_markup if blob.respond_to?(:rendered_markup)
.file-content.wiki
- = markup(blob.name, blob.data, rendered: rendered_markup)
+ = markup(blob.name, blob.data, context)
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index f880556a9f7..8da27ca7cb3 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -1,17 +1,17 @@
- unless @project.empty_repo?
- if current_user && can?(current_user, :fork_project, @project)
- - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
- = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn has-tooltip' do
- = custom_icon('icon_fork')
- %span= s_('GoToYourFork|Fork')
- - else
- - can_create_fork = current_user.can?(:create_fork)
- = link_to new_project_fork_path(@project),
- class: "btn btn-default #{'has-tooltip disabled' unless can_create_fork}",
- title: (_('You have reached your project limit') unless can_create_fork) do
- = custom_icon('icon_fork')
- %span= s_('CreateNewFork|Fork')
- .count-with-arrow
- %span.arrow
- = link_to project_forks_path(@project), title: n_('Fork', 'Forks', @project.forks_count), class: 'count' do
- = @project.forks_count
+ .count-badge.d-inline-flex.align-item-stretch.append-right-8
+ %span.fork-count.count-badge-count.d-flex.align-items-center
+ = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Fork'), s_('ProjectOverview|Forks'), @project.forks_count), class: 'count' do
+ = @project.forks_count
+ - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
+ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: s_('ProjectOverview|Go to your fork'), class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn' do
+ = sprite_icon('fork', { css_class: 'icon' })
+ %span= s_('ProjectOverview|Fork')
+ - else
+ - can_create_fork = current_user.can?(:create_fork)
+ = link_to new_project_fork_path(@project),
+ class: "btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}",
+ title: (s_('ProjectOverview|You have reached your project limit') unless can_create_fork) do
+ = sprite_icon('fork', { css_class: 'icon' })
+ %span= s_('ProjectOverview|Fork')
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index a2dc2730ecc..0d04ecb3a58 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,21 +1,19 @@
- if current_user
- %button.btn.btn-default.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } }>
- - if current_user.starred?(@project)
- = sprite_icon('star')
- %span.starred= _('Unstar')
- - else
- = sprite_icon('star-o')
- %span= s_('StarProject|Star')
- .count-with-arrow
- %span.arrow
- %span.count.star-count
+ .count-badge.d-inline-flex.align-item-stretch.append-right-8
+ %span.star-count.count-badge-count.d-flex.align-items-center
= @project.star_count
+ %button.count-badge-button.btn.btn-default.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } }
+ - if current_user.starred?(@project)
+ = sprite_icon('star', { css_class: 'icon' })
+ %span.starred= s_('ProjectOverview|Unstar')
+ - else
+ = sprite_icon('star-o', { css_class: 'icon' })
+ %span= s_('ProjectOverview|Star')
- else
- = link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: _('You must sign in to star a project') do
- = sprite_icon('star')
- #{ s_('StarProject|Star') }
- .count-with-arrow
- %span.arrow
- %span.count
+ .count-badge.d-inline-flex.align-item-stretch.append-right-8
+ %span.star-count.count-badge-count.d-flex.align-items-center
= @project.star_count
+ = link_to new_user_session_path, class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do
+ = sprite_icon('star-o', { css_class: 'icon' })
+ %span= s_('ProjectOverview|Star')
diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/projects/clusters/_banner.html.haml
index f18caa3f4ac..73cfea0ef92 100644
--- a/app/views/projects/clusters/_banner.html.haml
+++ b/app/views/projects/clusters/_banner.html.haml
@@ -1,14 +1,15 @@
-%h4= s_('ClusterIntegration|Kubernetes cluster integration')
+.hidden.js-cluster-error.bs-callout.bs-callout-danger{ role: 'alert' }
+ = s_('ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine')
+ %p.js-error-reason
-.settings-content
- .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
- = s_('ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine')
- %p.js-error-reason
+.hidden.js-cluster-creating.bs-callout.bs-callout-info{ role: 'alert' }
+ = s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...')
- .hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' }
- = s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...')
+.hidden.js-cluster-success.bs-callout.bs-callout-success{ role: 'alert' }
+ = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details")
- .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
- = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details")
-
- %p= s_('ClusterIntegration|Control how your Kubernetes cluster integrates with GitLab')
+- if show_cluster_security_warning?
+ .js-cluster-security-warning.alert.alert-block.alert-dismissable.bs-callout.bs-callout-warning
+ %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::CLUSTER_SECURITY_WARNING, dismiss_endpoint: user_callouts_path } } &times;
+ = s_("ClusterIntegration|The default cluster configuration grants access to many functionalities needed to successfully build and deploy a containerised application.")
+ = link_to s_("More information"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications')
diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/projects/clusters/_integration_form.html.haml
index b46b45fea49..d0a553e3414 100644
--- a/app/views/projects/clusters/_integration_form.html.haml
+++ b/app/views/projects/clusters/_integration_form.html.haml
@@ -2,14 +2,6 @@
= form_errors(@cluster)
.form-group
%h5= s_('ClusterIntegration|Integration status')
- %p
- - if @cluster.enabled?
- - if can?(current_user, :update_cluster, @cluster)
- = s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project. Disabling this integration will not affect your Kubernetes cluster, it will only temporarily turn off GitLab\'s connection to it.')
- - else
- = s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project.')
- - else
- = s_('ClusterIntegration|Kubernetes cluster integration is disabled for this project.')
%label.append-bottom-0.js-cluster-enable-toggle-area
%button{ type: 'button',
class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
@@ -19,14 +11,13 @@
%span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
+ .form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.')
- if has_multiple_clusters?(@project)
.form-group
%h5= s_('ClusterIntegration|Environment scope')
- %p
- = s_("ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster.")
- = link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments')
- = field.text_field :environment_scope, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope')
+ = field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope')
+ .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.")
- if can?(current_user, :update_cluster, @cluster)
.form-group
@@ -38,8 +29,3 @@
%code *
is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster.
= link_to 'More information', ('https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope')
-
- %h5= s_('ClusterIntegration|Security')
- %p
- = s_("ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application.")
- = link_to s_("ClusterIntegration|Learn more about security configuration"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications')
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index 08d2deff6f8..eddd3613c5f 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -23,7 +23,8 @@
.js-cluster-application-notice
.flash-container
- %section.settings.no-animate.expanded#cluster-integration
+ %section#cluster-integration
+ %h4= @cluster.name
= render 'banner'
= render 'integration_form'
diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml
index e8ef0008802..1f81e024ab9 100644
--- a/app/views/projects/clusters/user/_form.html.haml
+++ b/app/views/projects/clusters/user/_form.html.haml
@@ -25,5 +25,14 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+ - if rbac_clusters_feature_enabled?
+ .form-group
+ .form-check
+ = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input' }, 'rbac', 'abac'
+ = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
+ .form-text.text-muted
+ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
+ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+
.form-group
= field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml
index 20a07d6695e..56b597d295a 100644
--- a/app/views/projects/clusters/user/_show.html.haml
+++ b/app/views/projects/clusters/user/_show.html.haml
@@ -26,5 +26,14 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+ - if rbac_clusters_feature_enabled?
+ .form-group
+ .form-check
+ = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
+ = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
+ .form-text.text-muted
+ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
+ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+
.form-group
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/projects/default_branch/_show.html.haml b/app/views/projects/default_branch/_show.html.haml
new file mode 100644
index 00000000000..ff6a9d49a61
--- /dev/null
+++ b/app/views/projects/default_branch/_show.html.haml
@@ -0,0 +1,21 @@
+- expanded = Rails.env.test?
+
+%section.settings.no-animate#default-branch-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4= _('Default Branch')
+ %button.btn.js-settings-toggle
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Select the branch you want to set as the default for this project. All merge requests and commits will automatically be made against this branch unless you specify a different one.')
+
+ .settings-content
+ - if @project.empty_repo?
+ .text-secondary
+ = _('A default branch cannot be chosen for an empty project.')
+ - else
+ = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, anchor: 'default-branch-settings' }, authenticity_token: true do |f|
+ %fieldset
+ .form-group
+ = f.label :default_branch, "Default Branch", class: 'label-bold'
+ = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'})
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index fb837b27207..acdde9e0f70 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -36,11 +36,6 @@
= render_if_exists 'projects/classification_policy_settings', f: f
- - unless @project.empty_repo?
- .form-group
- = f.label :default_branch, "Default Branch", class: 'label-bold'
- = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'})
-
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project
.form-group
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index d47dc3d8143..d104608b2fe 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -32,9 +32,13 @@
= _('Otherwise it is recommended you start with one of the options below.')
.prepend-top-20
-%nav.project-stats{ class: container_class }
- = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
- = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
+%nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
+ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ .nav-links.scrolling-tabs
+ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
+ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
- if can?(current_user, :push_code, @project)
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
@@ -42,7 +46,7 @@
.empty_wrapper
%h3#repo-command-line-instructions.page-title-empty
Command line instructions
- .git-empty
+ .git-empty.js-git-empty
%fieldset
%h5 Git global setup
%pre.bg-light
@@ -54,7 +58,7 @@
%h5 Create a new repository
%pre.bg-light
:preserve
- git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
cd #{h @project.path}
touch README.md
git add README.md
@@ -69,7 +73,7 @@
:preserve
cd existing_folder
git init
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
git add .
git commit -m "Initial commit"
- if @project.can_current_user_push_to_default_branch?
@@ -82,7 +86,7 @@
:preserve
cd existing_repo
git remote rename origin old-origin
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- if @project.can_current_user_push_to_default_branch?
%span><
git push -u origin --all
diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml
index 60fe442014f..9a081a42b6f 100644
--- a/app/views/projects/issues/new.html.haml
+++ b/app/views/projects/issues/new.html.haml
@@ -1,4 +1,5 @@
-- breadcrumb_title "Issues"
+- add_to_breadcrumbs "Issues", project_issues_path(@project)
+- breadcrumb_title "New"
- page_title "New Issue"
%h3.page-title
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 2d036bd4e3e..b81d1a188f0 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -6,6 +6,7 @@
- page_card_attributes @issue.card_attributes
- can_update_issue = can?(current_user, :update_issue, @issue)
+- can_reopen_issue = can?(current_user, :reopen_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
- can_create_issue = show_new_issue_link?(@project)
@@ -40,6 +41,7 @@
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
%li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ - if can_reopen_issue
%li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- if can_report_spam
%li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
@@ -48,7 +50,7 @@
%li.divider
%li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
- = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue
+ = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue
- if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam'
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index a58179091ae..1bf42ded97a 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -39,4 +39,4 @@
- if can_update_merge_request
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-sm-none d-md-block btn btn-grouped js-issuable-edit"
- = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request
+ = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request, can_reopen: can_update_merge_request
diff --git a/app/views/projects/merge_requests/creations/new.html.haml b/app/views/projects/merge_requests/creations/new.html.haml
index 3220512d60d..0f618826305 100644
--- a/app/views/projects/merge_requests/creations/new.html.haml
+++ b/app/views/projects/merge_requests/creations/new.html.haml
@@ -1,4 +1,5 @@
-- breadcrumb_title "Merge Requests"
+- add_to_breadcrumbs "Merge Requests", project_merge_requests_path(@project)
+- breadcrumb_title "New"
- page_title "New Merge Request"
- if @merge_request.can_be_created && !params[:change_branches]
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 98c609d7bd4..a0bcaaf3c54 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -2,6 +2,7 @@
- page_title _("Repository")
- @content_class = "limit-container-width" unless fluid_layout
+= render "projects/default_branch/show"
= render "projects/mirrors/show"
-# Protected branches & tags use a lot of nested partials.
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index df8a5742450..aba289c790f 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -19,8 +19,13 @@
- if can?(current_user, :download_code, @project)
%nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
- = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
+ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ .nav-links.scrolling-tabs
+ = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
+ = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
+
= repository_languages_bar(@project.repository_languages)
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml
index abb3e918e87..406dccb74fb 100644
--- a/app/views/projects/tree/_tree_commit_column.html.haml
+++ b/app/views/projects/tree/_tree_commit_column.html.haml
@@ -1,2 +1,2 @@
%span.str-truncated
- = link_to_markdown commit.full_title, project_commit_path(@project, commit.id), class: "tree-commit-link"
+ = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), class: 'tree-commit-link'
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index de692466fe5..7fb80450161 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,9 +1,13 @@
- commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}")
- commit_message = commit_message % { page_title: @page.title }
+- if params[:legacy_render] || !commonmark_for_repositories_enabled?
+ - markdown_version = CacheMarkdownField::CACHE_REDCARPET_VERSION
+- else
+ - markdown_version = 0
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post,
html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' },
- data: { markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION } do |f|
+ data: { markdown_version: markdown_version } do |f|
= form_errors(@page)
- if @page.persisted?
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index 28353927135..02c5a6ea55c 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -12,7 +12,7 @@
.blocks-container
.block.block-first
- if @sidebar_page
- = render_wiki_content(@sidebar_page)
+ = render_wiki_content(@sidebar_page, legacy_render_context(params))
- else
%ul.wiki-pages
= render @sidebar_wiki_entries, context: 'sidebar'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index d80d2957466..71359708022 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -41,3 +41,8 @@
= render 'sidebar'
#delete-wiki-modal.modal.fade
+
+- content_for :scripts_body do
+ -# haml-lint:disable InlineJavaScript
+ :javascript
+ window.uploads_path = "#{wiki_attachment_upload_url}";
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index a08973c7f32..19b9744b508 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -26,6 +26,6 @@
.prepend-top-default.append-bottom-default
.wiki
- = render_wiki_content(@page)
+ = render_wiki_content(@page, legacy_render_context(params))
= render 'sidebar'
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index 57a0b64bfd5..8b95bdf9747 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -21,7 +21,7 @@
.file-content.wiki
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
- = markup(snippet.file_name, chunk[:data])
+ = markup(snippet.file_name, chunk[:data], legacy_render_context(params))
- else
.file-content.code
.nothing-here-block Empty file
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 3655c2a1d42..a2df0347fd6 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -1,14 +1,14 @@
- project = project || @project
-.git-clone-holder.input-group
+.git-clone-holder.js-git-clone-holder.input-group
.input-group-prepend
- if allowed_protocols_present?
.input-group-text.clone-dropdown-btn.btn
- %span
+ %span.js-clone-dropdown-label
= enabled_project_button(project, enabled_protocol)
- else
%a#clone-dropdown.input-group-text.btn.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
- %span
+ %span.js-clone-dropdown-label
= default_clone_protocol.upcase
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown
diff --git a/app/views/shared/_mobile_clone_panel.html.haml b/app/views/shared/_mobile_clone_panel.html.haml
new file mode 100644
index 00000000000..998985cabe1
--- /dev/null
+++ b/app/views/shared/_mobile_clone_panel.html.haml
@@ -0,0 +1,13 @@
+- project = project || @project
+- ssh_copy_label = _("Copy SSH clone URL")
+- http_copy_label = _("Copy HTTPS clone URL")
+
+.btn-group.mobile-git-clone.js-mobile-git-clone
+ = clipboard_button(button_text: default_clone_label, target: '#project_clone', hide_button_icon: true, class: "input-group-text clone-dropdown-btn js-clone-dropdown-label btn btn-default")
+ %button.btn.btn-default.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { toggle: "dropdown" } }
+ = icon("caret-down", class: "dropdown-btn-icon")
+ %ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown{ data: { dropdown: true } }
+ %li
+ = dropdown_item_with_description(ssh_copy_label, project.ssh_url_to_repo, href: project.ssh_url_to_repo, data: { clone_type: 'ssh' })
+ %li
+ = dropdown_item_with_description(http_copy_label, project.http_url_to_repo, href: project.http_url_to_repo, data: { clone_type: 'http' })
diff --git a/app/views/shared/groups/_empty_state.html.haml b/app/views/shared/groups/_empty_state.html.haml
index 13bb4baee3f..a9c78547eae 100644
--- a/app/views/shared/groups/_empty_state.html.haml
+++ b/app/views/shared/groups/_empty_state.html.haml
@@ -1,4 +1,4 @@
-.groups-empty-state
+.groups-empty-state.qa-groups-empty-state
= custom_icon("icon_empty_groups")
.text-content
diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml
index 933d4b2ea65..70e05eb1c8c 100644
--- a/app/views/shared/issuable/_close_reopen_button.html.haml
+++ b/app/views/shared/issuable/_close_reopen_button.html.haml
@@ -2,13 +2,15 @@
- display_issuable_type = issuable_display_type(issuable)
- button_method = issuable_close_reopen_button_method(issuable)
-- if can_update && is_current_user
- = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method,
- class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
- = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method,
- class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
-- elsif can_update && !is_current_user
- = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
+- if can_update
+ - if is_current_user
+ = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method,
+ class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
+ - else
+ = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
+ - if can_reopen && is_current_user
+ = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method,
+ class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
- else
= link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse'
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 07ebb8680d2..9c5b9593bba 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -17,6 +17,7 @@
%strong Push events
%p.light.ml-1
This URL will be triggered by a push to the repository
+ = form.text_field :push_events_branch_filter, class: 'form-control', placeholder: 'Branch name or wildcard pattern to trigger on (leave blank for all)'
%li
= form.check_box :tag_push_events, class: 'form-check-input'
= form.label :tag_push_events, class: 'list-label form-check-label ml-1' do
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index 6b1d75c6e72..2d4656e8608 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -18,7 +18,7 @@
= event_action_name(event)
%strong
- if event.note?
- = link_to event.note_target.to_reference, event_note_target_path(event), class: 'has-tooltip', title: event.target_title
+ = link_to event.note_target.to_reference, event_note_target_url(event), class: 'has-tooltip', title: event.target_title
- elsif event.target
= link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index f95df7ecf03..ae9dc8d4857 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1,4 +1,6 @@
---
+- auto_devops:auto_devops_disable
+
- cronjob:admin_email
- cronjob:expire_build_artifacts
- cronjob:gitlab_usage_ping
diff --git a/app/workers/auto_devops/disable_worker.rb b/app/workers/auto_devops/disable_worker.rb
new file mode 100644
index 00000000000..73ddc591505
--- /dev/null
+++ b/app/workers/auto_devops/disable_worker.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module AutoDevops
+ class DisableWorker
+ include ApplicationWorker
+ include AutoDevopsQueue
+
+ def perform(pipeline_id)
+ pipeline = Ci::Pipeline.find(pipeline_id)
+ project = pipeline.project
+
+ send_notification_email(pipeline, project) if disable_service(project).execute
+ end
+
+ private
+
+ def disable_service(project)
+ Projects::AutoDevops::DisableService.new(project)
+ end
+
+ def send_notification_email(pipeline, project)
+ recipients = email_receivers_for(pipeline, project)
+
+ return unless recipients.any?
+
+ NotificationService.new.autodevops_disabled(pipeline, recipients)
+ end
+
+ def email_receivers_for(pipeline, project)
+ recipients = [pipeline.user&.email]
+ recipients << project.owner.email unless project.group
+ recipients.uniq.compact
+ end
+ end
+end
diff --git a/app/workers/concerns/auto_devops_queue.rb b/app/workers/concerns/auto_devops_queue.rb
new file mode 100644
index 00000000000..aba928ccaab
--- /dev/null
+++ b/app/workers/concerns/auto_devops_queue.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+#
+module AutoDevopsQueue
+ extend ActiveSupport::Concern
+
+ included do
+ queue_namespace :auto_devops
+ end
+end