summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/commons/bootstrap.js1
-rw-r--r--app/assets/javascripts/dispatcher.js48
-rw-r--r--app/assets/javascripts/gpg_badges.js15
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js18
-rw-r--r--app/assets/javascripts/init_legacy_filters.js15
-rw-r--r--app/assets/javascripts/init_notes.js14
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js2
-rw-r--r--app/assets/javascripts/issuable_context.js6
-rw-r--r--app/assets/javascripts/main.js18
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js3
-rw-r--r--app/assets/javascripts/milestone_select.js2
-rw-r--r--app/assets/javascripts/project.js29
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js3
-rw-r--r--app/assets/javascripts/snippets_list.js9
-rw-r--r--app/assets/javascripts/todos.js4
-rw-r--r--app/assets/javascripts/users/activity_calendar.js239
-rw-r--r--app/assets/javascripts/users/index.js24
-rw-r--r--app/assets/javascripts/users/user.js34
-rw-r--r--app/assets/javascripts/users/user_tabs.js177
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss12
-rw-r--r--app/assets/stylesheets/framework/filters.scss3
-rw-r--r--app/assets/stylesheets/framework/mixins.scss26
-rw-r--r--app/assets/stylesheets/framework/typography.scss6
-rw-r--r--app/assets/stylesheets/pages/commits.scss60
-rw-r--r--app/assets/stylesheets/pages/profile.scss23
-rw-r--r--app/assets/stylesheets/pages/projects.scss1
-rw-r--r--app/assets/stylesheets/pages/status.scss21
-rw-r--r--app/controllers/admin/application_settings_controller.rb81
-rw-r--r--app/controllers/admin/applications_controller.rb2
-rw-r--r--app/controllers/application_controller.rb10
-rw-r--r--app/controllers/profiles/gpg_keys_controller.rb47
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/commits_controller.rb40
-rw-r--r--app/controllers/projects/wikis_controller.rb5
-rw-r--r--app/controllers/sessions_controller.rb7
-rw-r--r--app/controllers/users_controller.rb5
-rw-r--r--app/finders/issuable_finder.rb1
-rw-r--r--app/finders/merge_requests_finder.rb1
-rw-r--r--app/helpers/application_settings_helper.rb85
-rw-r--r--app/helpers/commits_helper.rb4
-rw-r--r--app/helpers/diff_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb10
-rw-r--r--app/helpers/notes_helper.rb10
-rw-r--r--app/mailers/emails/profile.rb12
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/chat_team.rb9
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/pipeline.rb1
-rw-r--r--app/models/ci/pipeline_variable.rb10
-rw-r--r--app/models/commit.rb12
-rw-r--r--app/models/concerns/issuable.rb5
-rw-r--r--app/models/gpg_key.rb107
-rw-r--r--app/models/gpg_signature.rb21
-rw-r--r--app/models/issue.rb5
-rw-r--r--app/models/merge_request.rb13
-rw-r--r--app/models/project_services/drone_ci_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb8
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/models/user.rb6
-rw-r--r--app/models/wiki_page.rb21
-rw-r--r--app/services/ci/create_pipeline_service.rb18
-rw-r--r--app/services/ci/create_trigger_request_service.rb5
-rw-r--r--app/services/ci/pipeline_trigger_service.rb44
-rw-r--r--app/services/git_push_service.rb8
-rw-r--r--app/services/groups/destroy_service.rb2
-rw-r--r--app/services/issues/reopen_service.rb6
-rw-r--r--app/services/merge_requests/base_service.rb6
-rw-r--r--app/services/merge_requests/reopen_service.rb2
-rw-r--r--app/services/notification_service.rb10
-rw-r--r--app/services/web_hook_service.rb2
-rw-r--r--app/services/wiki_pages/update_service.rb2
-rw-r--r--app/views/admin/applications/_form.html.haml8
-rw-r--r--app/views/admin/applications/index.html.haml2
-rw-r--r--app/views/admin/applications/show.html.haml6
-rw-r--r--app/views/groups/_shared_projects.html.haml1
-rw-r--r--app/views/layouts/help.html.haml1
-rw-r--r--app/views/layouts/nav/_new_profile_sidebar.html.haml4
-rw-r--r--app/views/layouts/nav/_profile.html.haml4
-rw-r--r--app/views/notify/new_gpg_key_email.html.haml10
-rw-r--r--app/views/notify/new_gpg_key_email.text.erb7
-rw-r--r--app/views/profiles/gpg_keys/_email_with_badge.html.haml8
-rw-r--r--app/views/profiles/gpg_keys/_form.html.haml10
-rw-r--r--app/views/profiles/gpg_keys/_key.html.haml18
-rw-r--r--app/views/profiles/gpg_keys/_key_table.html.haml11
-rw-r--r--app/views/profiles/gpg_keys/index.html.haml21
-rw-r--r--app/views/projects/blob/viewers/_image.html.haml2
-rw-r--r--app/views/projects/commit/_ajax_signature.html.haml3
-rw-r--r--app/views/projects/commit/_commit_box.html.haml1
-rw-r--r--app/views/projects/commit/_invalid_signature_badge.html.haml9
-rw-r--r--app/views/projects/commit/_signature.html.haml5
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml18
-rw-r--r--app/views/projects/commit/_valid_signature_badge.html.haml32
-rw-r--r--app/views/projects/commits/_commit.html.haml10
-rw-r--r--app/views/projects/commits/_commits.html.haml2
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/diffs/viewers/_image.html.haml14
-rw-r--r--app/views/projects/wikis/_form.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml6
-rw-r--r--app/views/shared/_clone_panel.html.haml8
-rw-r--r--app/views/shared/_label.html.haml8
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml15
-rw-r--r--app/views/shared/icons/_icon_status_notfound_borderless.svg1
-rw-r--r--app/views/shared/issuable/_filter.html.haml10
-rw-r--r--app/views/shared/issuable/_participants.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml9
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml15
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml3
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/snippets/_snippets.html.haml6
-rw-r--r--app/views/users/calendar.html.haml9
-rw-r--r--app/views/users/show.html.haml12
-rw-r--r--app/workers/create_gpg_signature_worker.rb16
-rw-r--r--app/workers/invalid_gpg_signature_update_worker.rb12
114 files changed, 1252 insertions, 564 deletions
diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js
index 36bfe457be9..510bedbf641 100644
--- a/app/assets/javascripts/commons/bootstrap.js
+++ b/app/assets/javascripts/commons/bootstrap.js
@@ -8,6 +8,7 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
+import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
// custom jQuery functions
$.fn.extend({
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 1dc6edacfed..9d706b5ba59 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -64,6 +64,10 @@ import initSettingsPanels from './settings_panels';
import initExperimentalFlags from './experimental_flags';
import OAuthRememberMe from './oauth_remember_me';
import PerformanceBar from './performance_bar';
+import initNotes from './init_notes';
+import initLegacyFilters from './init_legacy_filters';
+import initIssuableSidebar from './init_issuable_sidebar';
+import GpgBadges from './gpg_badges';
(function() {
var Dispatcher;
@@ -158,6 +162,8 @@ import PerformanceBar from './performance_bar';
new Issue();
shortcut_handler = new ShortcutsIssuable();
new ZenMode();
+ initIssuableSidebar();
+ initNotes();
break;
case 'dashboard:milestones:index':
new ProjectSelect();
@@ -168,10 +174,12 @@ import PerformanceBar from './performance_bar';
new Milestone();
new Sidebar();
break;
+ case 'dashboard:issues':
+ case 'dashboard:merge_requests':
case 'groups:issues':
case 'groups:merge_requests':
- new UsersSelect();
new ProjectSelect();
+ initLegacyFilters();
break;
case 'dashboard:todos:index':
new Todos();
@@ -252,6 +260,9 @@ import PerformanceBar from './performance_bar';
new gl.GLForm($('.tag-form'), true);
new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs);
break;
+ case 'projects:snippets:show':
+ initNotes();
+ break;
case 'projects:snippets:new':
case 'projects:snippets:edit':
case 'projects:snippets:create':
@@ -272,6 +283,10 @@ import PerformanceBar from './performance_bar';
new gl.Diff();
shortcut_handler = new ShortcutsIssuable(true);
new ZenMode();
+
+ initIssuableSidebar();
+ initNotes();
+
const mrShowNode = document.querySelector('.merge-request');
window.mergeRequest = new MergeRequest({
action: mrShowNode.dataset.mrAction,
@@ -280,11 +295,6 @@ import PerformanceBar from './performance_bar';
case 'dashboard:activity':
new gl.Activities();
break;
- case 'dashboard:issues':
- case 'dashboard:merge_requests':
- new ProjectSelect();
- new UsersSelect();
- break;
case 'projects:commit:show':
new Commit();
new gl.Diff();
@@ -293,6 +303,7 @@ import PerformanceBar from './performance_bar';
new MiniPipelineGraph({
container: '.js-commit-pipeline-graph',
}).bindEvents();
+ initNotes();
break;
case 'projects:commit:pipelines':
new MiniPipelineGraph({
@@ -300,6 +311,9 @@ import PerformanceBar from './performance_bar';
}).bindEvents();
break;
case 'projects:commits:show':
+ shortcut_handler = new ShortcutsNavigation();
+ GpgBadges.fetch();
+ break;
case 'projects:activity':
shortcut_handler = new ShortcutsNavigation();
break;
@@ -386,10 +400,20 @@ import PerformanceBar from './performance_bar';
case 'projects:labels:edit':
new Labels();
break;
+ case 'groups:labels:index':
case 'projects:labels:index':
if ($('.prioritized-labels').length) {
new gl.LabelManager();
}
+ $('.label-subscription').each((i, el) => {
+ const $el = $(el);
+
+ if ($el.find('.dropdown-group-label').length) {
+ new gl.GroupLabelSubscription($el);
+ } else {
+ new gl.ProjectLabelSubscription($el);
+ }
+ });
break;
case 'projects:network:show':
// Ensure we don't create a particular shortcut handler here. This is
@@ -434,10 +458,15 @@ import PerformanceBar from './performance_bar';
case 'snippets:show':
new LineHighlighter();
new BlobViewer();
+ initNotes();
break;
case 'import:fogbugz:new_user_map':
new UsersSelect();
break;
+ case 'profiles:personal_access_tokens:index':
+ case 'admin:impersonation_tokens:index':
+ new gl.DueDateSelectors();
+ break;
}
switch (path.first()) {
case 'sessions':
@@ -534,6 +563,13 @@ import PerformanceBar from './performance_bar';
case 'protected_branches':
shortcut_handler = new ShortcutsNavigation();
}
+ break;
+ case 'users':
+ const action = path[1];
+ import(/* webpackChunkName: 'user_profile' */ './users')
+ .then(user => user.default(action))
+ .catch(() => {});
+ break;
}
// If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js
new file mode 100644
index 00000000000..1c379e9bb67
--- /dev/null
+++ b/app/assets/javascripts/gpg_badges.js
@@ -0,0 +1,15 @@
+export default class GpgBadges {
+ static fetch() {
+ const form = $('.commits-search-form');
+
+ $.get({
+ url: form.data('signatures-path'),
+ data: form.serialize(),
+ }).done((response) => {
+ const badges = $('.js-loading-gpg-badge');
+ response.signatures.forEach((signature) => {
+ badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
new file mode 100644
index 00000000000..29e3d2ea94e
--- /dev/null
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -0,0 +1,18 @@
+/* eslint-disable no-new */
+/* global MilestoneSelect */
+/* global LabelsSelect */
+/* global IssuableContext */
+/* global Sidebar */
+
+export default () => {
+ const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
+
+ new MilestoneSelect({
+ full_path: sidebarOptions.fullPath,
+ });
+ new LabelsSelect();
+ new IssuableContext(sidebarOptions.currentUser);
+ gl.Subscription.bindAll('.subscription');
+ new gl.DueDateSelectors();
+ window.sidebar = new Sidebar();
+};
diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js
new file mode 100644
index 00000000000..1211c2c802c
--- /dev/null
+++ b/app/assets/javascripts/init_legacy_filters.js
@@ -0,0 +1,15 @@
+/* eslint-disable no-new */
+/* global LabelsSelect */
+/* global MilestoneSelect */
+/* global IssueStatusSelect */
+/* global SubscriptionSelect */
+
+import UsersSelect from './users_select';
+
+export default () => {
+ new UsersSelect();
+ new LabelsSelect();
+ new MilestoneSelect();
+ new IssueStatusSelect();
+ new SubscriptionSelect();
+};
diff --git a/app/assets/javascripts/init_notes.js b/app/assets/javascripts/init_notes.js
new file mode 100644
index 00000000000..3a8b4360cb6
--- /dev/null
+++ b/app/assets/javascripts/init_notes.js
@@ -0,0 +1,14 @@
+/* global Notes */
+
+export default () => {
+ const dataEl = document.querySelector('.js-notes-data');
+ const {
+ notesUrl,
+ notesIds,
+ now,
+ diffView,
+ autocomplete,
+ } = JSON.parse(dataEl.innerHTML);
+
+ window.notes = new Notes(notesUrl, notesIds, now, diffView, autocomplete);
+};
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index ddd3a6aab99..cf1e6a14725 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -102,7 +102,7 @@ export default class IntegrationSettingsForm {
})
.done((res) => {
if (res.error) {
- new Flash(res.message, null, null, {
+ new Flash(`${res.message} ${res.service_response}`, null, null, {
title: 'Save anyway',
clickHandler: (e) => {
e.preventDefault();
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index a4d7bf096ef..26392db4b5b 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -4,6 +4,8 @@
import Cookies from 'js-cookie';
import UsersSelect from './users_select';
+const PARTICIPANTS_ROW_COUNT = 7;
+
(function() {
this.IssuableContext = (function() {
function IssuableContext(currentUser) {
@@ -50,11 +52,9 @@ import UsersSelect from './users_select';
}
IssuableContext.prototype.initParticipants = function() {
- var _this;
- _this = this;
$(document).on("click", ".js-participants-more", this.toggleHiddenParticipants);
return $(".js-participants-author").each(function(i) {
- if (i >= _this.PARTICIPANTS_ROW_COUNT) {
+ if (i >= PARTICIPANTS_ROW_COUNT) {
return $(this).addClass("js-participants-hidden").hide();
}
});
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index e96d51de838..cd45091c211 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -145,7 +145,6 @@ import './right_sidebar';
import './search';
import './search_autocomplete';
import './smart_interval';
-import './snippets_list';
import './star';
import './subscription';
import './subscription_select';
@@ -159,6 +158,8 @@ document.addEventListener('beforeunload', function () {
$(document).off('scroll');
// Close any open tooltips
$('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
+ // Close any open popover
+ $('[data-toggle="popover"]').popover('destroy');
});
window.addEventListener('hashchange', gl.utils.handleLocationHash);
@@ -247,6 +248,11 @@ $(function () {
return $(el).data('placement') || 'bottom';
}
});
+ // Initialize popovers
+ $body.popover({
+ selector: '[data-toggle="popover"]',
+ trigger: 'focus'
+ });
$('.trigger-submit').on('change', function () {
return $(this).parents('form').submit();
// Form submitter
@@ -347,4 +353,14 @@ $(function () {
gl.utils.renderTimeago();
$(document).trigger('init.scrolling-tabs');
+
+ $('form.filter-form').on('submit', function (event) {
+ const link = document.createElement('a');
+ link.href = this.action;
+
+ const action = `${this.action}${link.search === '' ? '?' : '&'}`;
+
+ event.preventDefault();
+ gl.utils.visitUrl(`${action}${$(this).serialize()}`);
+ });
});
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index 17030c3e4d3..d74cf5328ad 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -2,6 +2,7 @@
/* global Flash */
import Vue from 'vue';
+import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store';
import './merge_conflict_service';
import './mixins/line_conflict_utils';
@@ -19,6 +20,8 @@ $(() => {
resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath
});
+ initIssuableSidebar();
+
gl.MergeConflictsResolverApp = new Vue({
el: '#conflicts',
data: mergeConflictsStore.state,
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 9d481d7c003..6756ab0b3aa 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -8,7 +8,7 @@
var _this, $els;
if (currentProject != null) {
_this = this;
- this.currentProject = JSON.parse(currentProject);
+ this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
}
$els = $(els);
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 738e710deb9..a3f7d69b98d 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -6,21 +6,22 @@ import Cookies from 'js-cookie';
(function() {
this.Project = (function() {
function Project() {
- $('ul.clone-options-dropdown a').click(function() {
- var url;
- if ($(this).hasClass('active')) {
- return;
- }
- $('.active').not($(this)).removeClass('active');
- $(this).toggleClass('active');
- url = $("#project_clone").val();
- $('#project_clone').val(url);
+ const $cloneOptions = $('ul.clone-options-dropdown');
+ const $projectCloneField = $('#project_clone');
+ const $cloneBtnText = $('a.clone-dropdown-btn span');
+
+ $('a', $cloneOptions).on('click', (e) => {
+ const $this = $(e.currentTarget);
+ const url = $this.attr('href');
+
+ e.preventDefault();
+
+ $('.active', $cloneOptions).not($this).removeClass('active');
+ $this.toggleClass('active');
+ $projectCloneField.val(url);
+ $cloneBtnText.text($this.text());
+
return $('.clone').text(url);
- // Git protocol switcher
- // Remove the active class for all buttons (ssh, http, kerberos if shown)
- // Add the active class for the clicked button
- // Update the input field
- // Update the command line instructions
});
// Ref switcher
this.initRefSwitcher();
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index 2b02af87d8a..a9df66748c5 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -5,7 +5,8 @@ import sidebarAssignees from './components/assignees/sidebar_assignees';
import Mediator from './sidebar_mediator';
function domContentLoaded() {
- const mediator = new Mediator(gl.sidebarOptions);
+ const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
+ const mediator = new Mediator(sidebarOptions);
mediator.fetch();
const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
diff --git a/app/assets/javascripts/snippets_list.js b/app/assets/javascripts/snippets_list.js
deleted file mode 100644
index 3b6d999b1c3..00000000000
--- a/app/assets/javascripts/snippets_list.js
+++ /dev/null
@@ -1,9 +0,0 @@
-function SnippetsList() {
- const $holder = $('.snippets-list-holder');
-
- $holder.find('.pagination').on('ajax:success', (e, data) => {
- $holder.replaceWith(data.html);
- });
-}
-
-window.gl.SnippetsList = SnippetsList;
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js
index cd305631c10..bba8b5abbb4 100644
--- a/app/assets/javascripts/todos.js
+++ b/app/assets/javascripts/todos.js
@@ -37,10 +37,6 @@ export default class Todos {
this.initFilterDropdown($('.js-type-search'), 'type');
this.initFilterDropdown($('.js-action-search'), 'action_id');
- $('form.filter-form').on('submit', function applyFilters(event) {
- event.preventDefault();
- gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`);
- });
return new UsersSelect();
}
diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/users/activity_calendar.js
index b7f50cfd083..f091e319f44 100644
--- a/app/assets/javascripts/users/activity_calendar.js
+++ b/app/assets/javascripts/users/activity_calendar.js
@@ -1,10 +1,28 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len, class-methods-use-this */
-
import d3 from 'd3';
+const LOADING_HTML = `
+ <div class="text-center">
+ <i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i>
+ </div>
+`;
+
+function formatTooltipText({ date, count }) {
+ const dateObject = new Date(date);
+ const dateDayName = gl.utils.getDayName(dateObject);
+ const dateText = dateObject.format('mmm d, yyyy');
+
+ let contribText = 'No contributions';
+ if (count > 0) {
+ contribText = `${count} contribution${count > 1 ? 's' : ''}`;
+ }
+ return `${contribText}<br />${dateDayName} ${dateText}`;
+}
+
+const initColorKey = () => d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]);
+
export default class ActivityCalendar {
- constructor(timestamps, calendar_activities_path) {
- this.calendar_activities_path = calendar_activities_path;
+ constructor(container, timestamps, calendarActivitiesPath) {
+ this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = '';
this.daySpace = 1;
@@ -12,25 +30,26 @@ export default class ActivityCalendar {
this.daySizeWithSpace = this.daySize + (this.daySpace * 2);
this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
this.months = [];
+
// Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are
this.timestampsTmp = [];
- var group = 0;
+ let group = 0;
- var today = new Date();
+ const today = new Date();
today.setHours(0, 0, 0, 0, 0);
- var oneYearAgo = new Date(today);
+ const oneYearAgo = new Date(today);
oneYearAgo.setFullYear(today.getFullYear() - 1);
- var days = gl.utils.getDayDifference(oneYearAgo, today);
+ const days = gl.utils.getDayDifference(oneYearAgo, today);
- for (var i = 0; i <= days; i += 1) {
- var date = new Date(oneYearAgo);
+ for (let i = 0; i <= days; i += 1) {
+ const date = new Date(oneYearAgo);
date.setDate(date.getDate() + i);
- var day = date.getDay();
- var count = timestamps[date.format('yyyy-mm-dd')];
+ const day = date.getDay();
+ const count = timestamps[date.format('yyyy-mm-dd')] || 0;
// Create a new group array if this is the first day of the week
// or if is first object
@@ -39,129 +58,119 @@ export default class ActivityCalendar {
group += 1;
}
- var innerArray = this.timestampsTmp[group - 1];
// Push to the inner array the values that will be used to render map
- innerArray.push({
- count: count || 0,
- date: date,
- day: day
- });
+ const innerArray = this.timestampsTmp[group - 1];
+ innerArray.push({ count, date, day });
}
// Init color functions
- this.colorKey = this.initColorKey();
+ this.colorKey = initColorKey();
this.color = this.initColor();
+
// Init the svg element
- this.renderSvg(group);
+ this.svg = this.renderSvg(container, group);
this.renderDays();
this.renderMonths();
this.renderDayTitles();
this.renderKey();
- this.initTooltips();
+
+ // Init tooltips
+ $(`${container} .js-tooltip`).tooltip({ html: true });
}
// Add extra padding for the last month label if it is also the last column
getExtraWidthPadding(group) {
- var extraWidthPadding = 0;
- var lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth();
- var secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth();
+ let extraWidthPadding = 0;
+ const lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth();
+ const secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth();
- if (lastColMonth != secondLastColMonth) {
+ if (lastColMonth !== secondLastColMonth) {
extraWidthPadding = 3;
}
return extraWidthPadding;
}
- renderSvg(group) {
- var width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
- return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', width).attr('height', 167).attr('class', 'contrib-calendar');
+ renderSvg(container, group) {
+ const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group);
+ return d3.select(container)
+ .append('svg')
+ .attr('width', width)
+ .attr('height', 167)
+ .attr('class', 'contrib-calendar');
}
renderDays() {
- return this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g').attr('transform', (function(_this) {
- return function(group, i) {
- _.each(group, function(stamp, a) {
- var lastMonth, lastMonthX, month, x;
+ this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g')
+ .attr('transform', (group, i) => {
+ _.each(group, (stamp, a) => {
if (a === 0 && stamp.day === 0) {
- month = stamp.date.getMonth();
- x = (_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace;
- lastMonth = _.last(_this.months);
- if (lastMonth != null) {
- lastMonthX = lastMonth.x;
- }
- if (lastMonth == null) {
- return _this.months.push({
- month: month,
- x: x
- });
- } else if (month !== lastMonth.month && x - _this.daySizeWithSpace !== lastMonthX) {
- return _this.months.push({
- month: month,
- x: x
- });
+ const month = stamp.date.getMonth();
+ const x = (this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace;
+ const lastMonth = _.last(this.months);
+ if (
+ lastMonth == null ||
+ (month !== lastMonth.month && x - this.daySizeWithSpace !== lastMonth.x)
+ ) {
+ this.months.push({ month, x });
}
}
});
- return "translate(" + ((_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace) + ", 18)";
- };
- })(this)).selectAll('rect').data(function(stamp) {
- return stamp;
- }).enter().append('rect').attr('x', '0').attr('y', (function(_this) {
- return function(stamp, i) {
- return _this.daySizeWithSpace * stamp.day;
- };
- })(this)).attr('width', this.daySize).attr('height', this.daySize).attr('title', (function(_this) {
- return function(stamp) {
- var contribText, date, dateText;
- date = new Date(stamp.date);
- contribText = 'No contributions';
- if (stamp.count > 0) {
- contribText = stamp.count + " contribution" + (stamp.count > 1 ? 's' : '');
- }
- dateText = date.format('mmm d, yyyy');
- return contribText + "<br />" + (gl.utils.getDayName(date)) + " " + dateText;
- };
- })(this)).attr('class', 'user-contrib-cell js-tooltip').attr('fill', (function(_this) {
- return function(stamp) {
- if (stamp.count !== 0) {
- return _this.color(Math.min(stamp.count, 40));
- } else {
- return '#ededed';
- }
- };
- })(this)).attr('data-container', 'body').on('click', this.clickDay);
+ return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`;
+ })
+ .selectAll('rect')
+ .data(stamp => stamp)
+ .enter()
+ .append('rect')
+ .attr('x', '0')
+ .attr('y', stamp => this.daySizeWithSpace * stamp.day)
+ .attr('width', this.daySize)
+ .attr('height', this.daySize)
+ .attr('fill', stamp => (
+ stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'
+ ))
+ .attr('title', stamp => formatTooltipText(stamp))
+ .attr('class', 'user-contrib-cell js-tooltip')
+ .attr('data-container', 'body')
+ .on('click', this.clickDay);
}
renderDayTitles() {
- var days;
- days = [
+ const days = [
{
text: 'M',
- y: 29 + (this.daySizeWithSpace * 1)
+ y: 29 + (this.daySizeWithSpace * 1),
}, {
text: 'W',
- y: 29 + (this.daySizeWithSpace * 3)
+ y: 29 + (this.daySizeWithSpace * 3),
}, {
text: 'F',
- y: 29 + (this.daySizeWithSpace * 5)
- }
+ y: 29 + (this.daySizeWithSpace * 5),
+ },
];
- return this.svg.append('g').selectAll('text').data(days).enter().append('text').attr('text-anchor', 'middle').attr('x', 8).attr('y', function(day) {
- return day.y;
- }).text(function(day) {
- return day.text;
- }).attr('class', 'user-contrib-text');
+ this.svg.append('g')
+ .selectAll('text')
+ .data(days)
+ .enter()
+ .append('text')
+ .attr('text-anchor', 'middle')
+ .attr('x', 8)
+ .attr('y', day => day.y)
+ .text(day => day.text)
+ .attr('class', 'user-contrib-text');
}
renderMonths() {
- return this.svg.append('g').attr('direction', 'ltr').selectAll('text').data(this.months).enter().append('text').attr('x', function(date) {
- return date.x;
- }).attr('y', 10).attr('class', 'user-contrib-text').text((function(_this) {
- return function(date) {
- return _this.monthNames[date.month];
- };
- })(this));
+ this.svg.append('g')
+ .attr('direction', 'ltr')
+ .selectAll('text')
+ .data(this.months)
+ .enter()
+ .append('text')
+ .attr('x', date => date.x)
+ .attr('y', 10)
+ .attr('class', 'user-contrib-text')
+ .text(date => this.monthNames[date.month]);
}
renderKey() {
@@ -169,7 +178,7 @@ export default class ActivityCalendar {
const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
this.svg.append('g')
- .attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
+ .attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`)
.selectAll('rect')
.data(keyColors)
.enter()
@@ -185,43 +194,31 @@ export default class ActivityCalendar {
}
initColor() {
- var colorRange;
- colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
+ const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
return d3.scale.threshold().domain([0, 10, 20, 30]).range(colorRange);
}
- initColorKey() {
- return d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]);
- }
-
clickDay(stamp) {
- var formatted_date;
if (this.currentSelectedDate !== stamp.date) {
this.currentSelectedDate = stamp.date;
- formatted_date = this.currentSelectedDate.getFullYear() + "-" + (this.currentSelectedDate.getMonth() + 1) + "-" + this.currentSelectedDate.getDate();
- return $.ajax({
- url: this.calendar_activities_path,
- data: {
- date: formatted_date
- },
+
+ const date = [
+ this.currentSelectedDate.getFullYear(),
+ this.currentSelectedDate.getMonth() + 1,
+ this.currentSelectedDate.getDate(),
+ ].join('-');
+
+ $.ajax({
+ url: this.calendarActivitiesPath,
+ data: { date },
cache: false,
dataType: 'html',
- beforeSend: function() {
- return $('.user-calendar-activities').html('<div class="text-center"><i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i></div>');
- },
- success: function(data) {
- return $('.user-calendar-activities').html(data);
- }
+ beforeSend: () => $('.user-calendar-activities').html(LOADING_HTML),
+ success: data => $('.user-calendar-activities').html(data),
});
} else {
this.currentSelectedDate = '';
- return $('.user-calendar-activities').html('');
+ $('.user-calendar-activities').html('');
}
}
-
- initTooltips() {
- return $('.js-contrib-calendar .js-tooltip').tooltip({
- html: true
- });
- }
}
diff --git a/app/assets/javascripts/users/index.js b/app/assets/javascripts/users/index.js
index ecd8e09161e..33a83f8dae5 100644
--- a/app/assets/javascripts/users/index.js
+++ b/app/assets/javascripts/users/index.js
@@ -1,7 +1,19 @@
-import ActivityCalendar from './activity_calendar';
-import User from './user';
+import Cookies from 'js-cookie';
+import UserTabs from './user_tabs';
-// use legacy exports until embedded javascript is refactored
-window.Calendar = ActivityCalendar;
-window.gl = window.gl || {};
-window.gl.User = User;
+export default function initUserProfile(action) {
+ // place profile avatars to top
+ $('.profile-groups-avatars').tooltip({
+ placement: 'top',
+ });
+
+ // eslint-disable-next-line no-new
+ new UserTabs({ parentEl: '.user-profile', action });
+
+ // hide project limit message
+ $('.hide-project-limit-message').on('click', (e) => {
+ e.preventDefault();
+ Cookies.set('hide_project_limit_message', 'false');
+ $(this).parents('.project-limit-message').remove();
+ });
+}
diff --git a/app/assets/javascripts/users/user.js b/app/assets/javascripts/users/user.js
deleted file mode 100644
index 0b0a3e1afb4..00000000000
--- a/app/assets/javascripts/users/user.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/* eslint-disable class-methods-use-this */
-
-import Cookies from 'js-cookie';
-import UserTabs from './user_tabs';
-
-export default class User {
- constructor({ action }) {
- this.action = action;
- this.placeProfileAvatarsToTop();
- this.initTabs();
- this.hideProjectLimitMessage();
- }
-
- placeProfileAvatarsToTop() {
- $('.profile-groups-avatars').tooltip({
- placement: 'top',
- });
- }
-
- initTabs() {
- return new UserTabs({
- parentEl: '.user-profile',
- action: this.action,
- });
- }
-
- hideProjectLimitMessage() {
- $('.hide-project-limit-message').on('click', (e) => {
- e.preventDefault();
- Cookies.set('hide_project_limit_message', 'false');
- $(this).parents('.project-limit-message').remove();
- });
- }
-}
diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/users/user_tabs.js
index f8e23c8624d..5fe6603ce7b 100644
--- a/app/assets/javascripts/users/user_tabs.js
+++ b/app/assets/javascripts/users/user_tabs.js
@@ -1,72 +1,76 @@
-/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, no-param-reassign, class-methods-use-this */
-
-/*
-UserTabs
-
-Handles persisting and restoring the current tab selection and lazily-loading
-content on the Users#show page.
-
-### Example Markup
-
- <ul class="nav-links">
- <li class="activity-tab active">
- <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
- Activity
- </a>
- </li>
- <li class="groups-tab">
- <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
- Groups
- </a>
- </li>
- <li class="contributed-tab">
- <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
- Contributed projects
- </a>
- </li>
- <li class="projects-tab">
- <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
- Personal projects
- </a>
- </li>
- <li class="snippets-tab">
- <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
- </a>
- </li>
- </ul>
-
- <div class="tab-content">
- <div class="tab-pane" id="activity">
- Activity Content
- </div>
- <div class="tab-pane" id="groups">
- Groups Content
- </div>
- <div class="tab-pane" id="contributed">
- Contributed projects content
- </div>
- <div class="tab-pane" id="projects">
- Projects content
- </div>
- <div class="tab-pane" id="snippets">
- Snippets content
- </div>
+import ActivityCalendar from './activity_calendar';
+
+/**
+ * UserTabs
+ *
+ * Handles persisting and restoring the current tab selection and lazily-loading
+ * content on the Users#show page.
+ *
+ * ### Example Markup
+ *
+ * <ul class="nav-links">
+ * <li class="activity-tab active">
+ * <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
+ * Activity
+ * </a>
+ * </li>
+ * <li class="groups-tab">
+ * <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
+ * Groups
+ * </a>
+ * </li>
+ * <li class="contributed-tab">
+ * ...
+ * </li>
+ * <li class="projects-tab">
+ * ...
+ * </li>
+ * <li class="snippets-tab">
+ * ...
+ * </li>
+ * </ul>
+ *
+ * <div class="tab-content">
+ * <div class="tab-pane" id="activity">
+ * Activity Content
+ * </div>
+ * <div class="tab-pane" id="groups">
+ * Groups Content
+ * </div>
+ * <div class="tab-pane" id="contributed">
+ * Contributed projects content
+ * </div>
+ * <div class="tab-pane" id="projects">
+ * Projects content
+ * </div>
+ * <div class="tab-pane" id="snippets">
+ * Snippets content
+ * </div>
+ * </div>
+ *
+ * <div class="loading-status">
+ * <div class="loading">
+ * Loading Animation
+ * </div>
+ * </div>
+ */
+
+const CALENDAR_TEMPLATE = `
+ <div class="clearfix calendar">
+ <div class="js-contrib-calendar"></div>
+ <div class="calendar-hint">
+ Summary of issues, merge requests, push events, and comments
+ </div>
</div>
-
- <div class="loading-status">
- <div class="loading">
- Loading Animation
- </div>
- </div>
-*/
+`;
export default class UserTabs {
- constructor ({ defaultAction, action, parentEl }) {
+ constructor({ defaultAction, action, parentEl }) {
this.loaded = {};
this.defaultAction = defaultAction || 'activity';
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
- this._location = window.location;
+ this.windowLocation = window.location;
this.$parentEl.find('.nav-links a')
.each((i, navLink) => {
this.loaded[$(navLink).attr('data-action')] = false;
@@ -82,12 +86,10 @@ export default class UserTabs {
}
bindEvents() {
- this.changeProjectsPageWrapper = this.changeProjectsPage.bind(this);
-
- this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
- .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
-
- this.$parentEl.on('click', '.gl-pagination a', this.changeProjectsPageWrapper);
+ this.$parentEl
+ .off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
+ .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event))
+ .on('click', '.gl-pagination a', event => this.changeProjectsPage(event));
}
changeProjectsPage(e) {
@@ -122,7 +124,7 @@ export default class UserTabs {
const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
if (loadableActions.indexOf(action) > -1) {
- return this.loadTab(action, endpoint);
+ this.loadTab(action, endpoint);
}
}
@@ -131,25 +133,38 @@ export default class UserTabs {
beforeSend: () => this.toggleLoading(true),
complete: () => this.toggleLoading(false),
dataType: 'json',
- type: 'GET',
url: endpoint,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
- return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
- }
+ gl.utils.localTimeAgo($('.js-timeago', tabSelector));
+ },
});
}
loadActivities() {
- if (this.loaded['activity']) {
+ if (this.loaded.activity) {
return;
}
const $calendarWrap = this.$parentEl.find('.user-calendar');
- $calendarWrap.load($calendarWrap.data('href'));
+ const calendarPath = $calendarWrap.data('calendarPath');
+ const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath');
+
+ $.ajax({
+ dataType: 'json',
+ url: calendarPath,
+ success: (activityData) => {
+ $calendarWrap.html(CALENDAR_TEMPLATE);
+
+ // eslint-disable-next-line no-new
+ new ActivityCalendar('.js-contrib-calendar', activityData, calendarActivitiesPath);
+ },
+ });
+
+ // eslint-disable-next-line no-new
new gl.Activities();
- return this.loaded['activity'] = true;
+ this.loaded.activity = true;
}
toggleLoading(status) {
@@ -158,13 +173,13 @@ export default class UserTabs {
}
setCurrentAction(source) {
- let new_state = source;
- new_state = new_state.replace(/\/+$/, '');
- new_state += this._location.search + this._location.hash;
+ let newState = source;
+ newState = newState.replace(/\/+$/, '');
+ newState += this.windowLocation.search + this.windowLocation.hash;
history.replaceState({
- url: new_state
- }, document.title, new_state);
- return new_state;
+ url: newState,
+ }, document.title, newState);
+ return newState;
}
getCurrentAction() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 72a13108404..fddafb0ddfa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -65,7 +65,7 @@ export default class MergeRequestStore {
this.mergeCheckPath = data.merge_check_path;
this.mergeActionsContentPath = data.commit_change_content_path;
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
- this.isOpen = data.state === 'opened' || data.state === 'reopened' || false;
+ this.isOpen = data.state === 'opened';
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false;
this.canMerge = !!data.merge_path;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 5e410cbf563..3f934403147 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -148,7 +148,6 @@
padding: 5px 8px;
color: $gl-text-color;
line-height: initial;
- text-overflow: ellipsis;
border-radius: 2px;
white-space: nowrap;
overflow: hidden;
@@ -203,11 +202,6 @@
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
- @media (max-width: $screen-sm-min) {
- width: 100%;
- min-width: 180px;
- }
-
&.dropdown-open-left {
right: 0;
left: auto;
@@ -289,6 +283,11 @@
padding: 5px 8px;
color: $gl-text-color-secondary;
}
+
+ .badge + span:not(.badge) {
+ // Expects up to 3 digits on the badge
+ margin-right: 40px;
+ }
}
.droplab-dropdown {
@@ -373,7 +372,6 @@
.dropdown-menu,
.dropdown-menu-nav {
max-width: 280px;
- width: auto;
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 41184907abb..ab2abaca33a 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -393,7 +393,8 @@
@media (max-width: $screen-xs) {
.filter-dropdown-container {
.dropdown-toggle,
- .dropdown {
+ .dropdown,
+ .dropdown-menu {
width: 100%;
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 3a98332e46c..6f91d11b369 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -118,3 +118,29 @@
@content;
}
}
+
+/*
+ * Mixin for status badges, as used for pipelines and commit signatures
+ */
+@mixin status-color($color-light, $color-main, $color-dark) {
+ color: $color-main;
+ border-color: $color-main;
+
+ &:not(span):hover {
+ background-color: $color-light;
+ color: $color-dark;
+ border-color: $color-dark;
+
+ svg {
+ fill: $color-dark;
+ }
+ }
+
+ svg {
+ fill: $color-main;
+ }
+}
+
+@mixin green-status-color {
+ @include status-color($green-50, $green-500, $green-700);
+}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index befd8133be0..bf5f124d142 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -19,9 +19,9 @@
}
img.js-lazy-loaded {
- min-width: none;
- min-height: none;
- background-color: none;
+ min-width: inherit;
+ min-height: inherit;
+ background-color: inherit;
}
p a:not(.no-attachment-icon) img {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index fd0871ec0b8..cd9f2d787c5 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -283,3 +283,63 @@
color: $gl-text-color;
}
}
+
+
+.gpg-status-box {
+ &.valid {
+ @include green-status-color;
+ }
+
+ &.invalid {
+ @include status-color($gray-dark, $gray, $common-gray-dark);
+ border-color: $common-gray-light;
+ }
+}
+
+.gpg-popover-status {
+ display: flex;
+ align-items: center;
+ font-weight: normal;
+ line-height: 1.5;
+}
+
+.gpg-popover-icon {
+ // same margin as .s32.avatar
+ margin-right: $btn-side-margin;
+
+ &.valid {
+ svg {
+ border: 1px solid $brand-success;
+
+ fill: $brand-success;
+ }
+ }
+
+ &.invalid {
+ svg {
+ border: 1px solid $common-gray-light;
+
+ fill: $common-gray-light;
+ }
+ }
+
+ svg {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ vertical-align: middle;
+ }
+}
+
+.gpg-popover-user-link {
+ display: flex;
+ align-items: center;
+ margin-bottom: $gl-padding / 2;
+ text-decoration: none;
+ color: $gl-text-color;
+}
+
+.commit .gpg-popover-help-link {
+ display: block;
+ color: $link-color;
+}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 22672614e0d..14ad06b0ac2 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -391,3 +391,26 @@ table.u2f-registrations {
margin-bottom: 0;
}
}
+
+.gpg-email-badge {
+ display: inline;
+ margin-right: $gl-padding / 2;
+
+ .gpg-email-badge-email {
+ display: inline;
+ margin-right: $gl-padding / 4;
+ }
+
+ .label-verification-status {
+ border-width: 1px;
+ border-style: solid;
+
+ &.verified {
+ @include green-status-color;
+ }
+
+ &.unverified {
+ @include status-color($gray-dark, $gray, $common-gray-dark);
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index a3e07a36c33..b3a90dff89a 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -707,6 +707,7 @@ pre.light-well {
background-color: transparent;
border: 0;
text-align: left;
+ text-overflow: ellipsis;
}
.protected-branches-list,
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 67ad1ae60af..36f622db136 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -1,22 +1,3 @@
-@mixin status-color($color-light, $color-main, $color-dark) {
- color: $color-main;
- border-color: $color-main;
-
- &:not(span):hover {
- background-color: $color-light;
- color: $color-dark;
- border-color: $color-dark;
-
- svg {
- fill: $color-dark;
- }
- }
-
- svg {
- fill: $color-main;
- }
-}
-
.ci-status {
padding: 2px 7px 4px;
border: 1px solid $gray-darker;
@@ -41,7 +22,7 @@
}
&.ci-success {
- @include status-color($green-50, $green-500, $green-700);
+ @include green-status-color;
}
&.ci-canceled,
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 4c0f7556894..8367c22d1ca 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -76,88 +76,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
params.require(:application_setting).permit(
- application_setting_params_attributes
+ visible_application_setting_attributes
)
end
- def application_setting_params_attributes
- [
- :admin_notification_email,
- :after_sign_out_path,
- :after_sign_up_text,
- :akismet_api_key,
- :akismet_enabled,
- :container_registry_token_expire_delay,
- :default_artifacts_expire_in,
- :default_branch_protection,
- :default_group_visibility,
- :default_project_visibility,
- :default_projects_limit,
- :default_snippet_visibility,
- :domain_blacklist_enabled,
+ def visible_application_setting_attributes
+ ApplicationSettingsHelper.visible_attributes + [
:domain_blacklist_file,
- :domain_blacklist_raw,
- :domain_whitelist_raw,
- :email_author_in_body,
- :enabled_git_access_protocol,
- :gravatar_enabled,
- :help_page_text,
- :help_page_hide_commercial_content,
- :help_page_support_url,
- :home_page_url,
- :housekeeping_bitmaps_enabled,
- :housekeeping_enabled,
- :housekeeping_full_repack_period,
- :housekeeping_gc_period,
- :housekeeping_incremental_repack_period,
- :html_emails_enabled,
- :koding_enabled,
- :koding_url,
- :password_authentication_enabled,
- :plantuml_enabled,
- :plantuml_url,
- :max_artifacts_size,
- :max_attachment_size,
- :max_pages_size,
- :metrics_enabled,
- :metrics_host,
- :metrics_method_call_threshold,
- :metrics_packet_size,
- :metrics_pool_size,
- :metrics_port,
- :metrics_sample_interval,
- :metrics_timeout,
- :performance_bar_allowed_group_id,
- :performance_bar_enabled,
- :recaptcha_enabled,
- :recaptcha_private_key,
- :recaptcha_site_key,
- :repository_checks_enabled,
- :require_two_factor_authentication,
- :session_expire_delay,
- :sign_in_text,
- :signup_enabled,
- :sentry_dsn,
- :sentry_enabled,
- :clientside_sentry_dsn,
- :clientside_sentry_enabled,
- :send_user_confirmation_email,
- :shared_runners_enabled,
- :shared_runners_text,
- :sidekiq_throttling_enabled,
- :sidekiq_throttling_factor,
- :two_factor_grace_period,
- :user_default_external,
- :user_oauth_applications,
- :unique_ips_limit_per_user,
- :unique_ips_limit_time_window,
- :unique_ips_limit_enabled,
- :version_check_enabled,
- :terminal_max_session_time,
- :polling_interval_multiplier,
- :prometheus_metrics_enabled,
- :usage_ping_enabled,
-
disabled_oauth_sign_in_sources: [],
import_sources: [],
repository_storages: [],
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 434ff6b2a62..16590e66d61 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -50,6 +50,6 @@ class Admin::ApplicationsController < Admin::ApplicationController
# Only allow a trusted parameter "white list" through.
def application_params
- params[:doorkeeper_application].permit(:name, :redirect_uri, :scopes)
+ params.require(:doorkeeper_application).permit(:name, :redirect_uri, :trusted, :scopes)
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 43462b13903..d14b1dbecf6 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -70,6 +70,16 @@ class ApplicationController < ActionController::Base
protected
+ def append_info_to_payload(payload)
+ super
+ payload[:remote_ip] = request.remote_ip
+
+ if current_user.present?
+ payload[:user_id] = current_user.id
+ payload[:username] = current_user.username
+ end
+ end
+
# This filter handles both private tokens and personal access tokens
def authenticate_user_from_private_token!
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb
new file mode 100644
index 00000000000..6779cc6ddac
--- /dev/null
+++ b/app/controllers/profiles/gpg_keys_controller.rb
@@ -0,0 +1,47 @@
+class Profiles::GpgKeysController < Profiles::ApplicationController
+ before_action :set_gpg_key, only: [:destroy, :revoke]
+
+ def index
+ @gpg_keys = current_user.gpg_keys
+ @gpg_key = GpgKey.new
+ end
+
+ def create
+ @gpg_key = current_user.gpg_keys.new(gpg_key_params)
+
+ if @gpg_key.save
+ redirect_to profile_gpg_keys_path
+ else
+ @gpg_keys = current_user.gpg_keys.select(&:persisted?)
+ render :index
+ end
+ end
+
+ def destroy
+ @gpg_key.destroy
+
+ respond_to do |format|
+ format.html { redirect_to profile_gpg_keys_url, status: 302 }
+ format.js { head :ok }
+ end
+ end
+
+ def revoke
+ @gpg_key.revoke
+
+ respond_to do |format|
+ format.html { redirect_to profile_gpg_keys_url, status: 302 }
+ format.js { head :ok }
+ end
+ end
+
+ private
+
+ def gpg_key_params
+ params.require(:gpg_key).permit(:key)
+ end
+
+ def set_gpg_key
+ @gpg_key = current_user.gpg_keys.find(params[:id])
+ end
+end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 86058531179..747768eefb1 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -8,7 +8,7 @@ class Projects::BranchesController < Projects::ApplicationController
before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged]
def index
- @sort = params[:sort].presence || sort_value_name
+ @sort = params[:sort].presence || sort_value_recently_updated
@branches = BranchesFinder.new(@repository, params).execute
@branches = Kaminari.paginate_array(@branches).page(params[:page])
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 37b5a6e9d48..2de9900d449 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -6,18 +6,9 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_download_code!
+ before_action :set_commits
def show
- @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
- search = params[:search]
-
- @commits =
- if search.present?
- @repository.find_commits_by_message(search, @ref, @path, @limit, @offset)
- else
- @repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
- end
-
@note_counts = project.notes.where(commit_id: @commits.map(&:id))
.group(:commit_id).count
@@ -37,4 +28,33 @@ class Projects::CommitsController < Projects::ApplicationController
end
end
end
+
+ def signatures
+ respond_to do |format|
+ format.json do
+ render json: {
+ signatures: @commits.select(&:has_signature?).map do |commit|
+ {
+ commit_sha: commit.sha,
+ html: view_to_html_string('projects/commit/_signature', signature: commit.signature)
+ }
+ end
+ }
+ end
+ end
+ end
+
+ private
+
+ def set_commits
+ @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
+ search = params[:search]
+
+ @commits =
+ if search.present?
+ @repository.find_commits_by_message(search, @ref, @path, @limit, @offset)
+ else
+ @repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
+ end
+ end
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index ac98470c2b1..968d880886c 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -55,6 +55,9 @@ class Projects::WikisController < Projects::ApplicationController
else
render 'edit'
end
+ rescue WikiPage::PageChangedError
+ @conflict = true
+ render 'edit'
end
def create
@@ -119,6 +122,6 @@ class Projects::WikisController < Projects::ApplicationController
end
def wiki_params
- params.require(:wiki).permit(:title, :content, :format, :message)
+ params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha)
end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 69513f4dadc..9e743685d60 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -23,12 +23,7 @@ class SessionsController < Devise::SessionsController
def new
set_minimum_password_length
- @ldap_servers =
- if Gitlab.config.ldap.enabled
- Gitlab::LDAP::Config.servers
- else
- []
- end
+ @ldap_servers = Gitlab::LDAP::Config.available_servers
super
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 8131eba6a2f..4ee855806ab 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -73,10 +73,7 @@ class UsersController < ApplicationController
end
def calendar
- calendar = contributions_calendar
- @activity_dates = calendar.activity_dates
-
- render 'calendar', layout: false
+ render json: contributions_calendar.activity_dates
end
def calendar_activities
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 6fe17a2b99d..08a843ada97 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -81,7 +81,6 @@ class IssuableFinder
end
counts[:all] = counts.values.sum
- counts[:opened] += counts[:reopened]
counts
end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 2fc34f186ad..771da3d441d 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -10,6 +10,7 @@
# group_id: integer
# project_id: integer
# milestone_title: string
+# author_id: integer
# assignee_id: integer
# search: string
# label_name: string
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 29b88c60dab..6825adcb39f 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -1,4 +1,5 @@
module ApplicationSettingsHelper
+ extend self
delegate :gravatar_enabled?,
:signup_enabled?,
:password_authentication_enabled?,
@@ -91,4 +92,88 @@ module ApplicationSettingsHelper
def sidekiq_queue_options_for_select
options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues)
end
+
+ def visible_attributes
+ [
+ :admin_notification_email,
+ :after_sign_out_path,
+ :after_sign_up_text,
+ :akismet_api_key,
+ :akismet_enabled,
+ :clientside_sentry_dsn,
+ :clientside_sentry_enabled,
+ :container_registry_token_expire_delay,
+ :default_artifacts_expire_in,
+ :default_branch_protection,
+ :default_group_visibility,
+ :default_project_visibility,
+ :default_projects_limit,
+ :default_snippet_visibility,
+ :disabled_oauth_sign_in_sources,
+ :domain_blacklist_enabled,
+ :domain_blacklist_raw,
+ :domain_whitelist_raw,
+ :email_author_in_body,
+ :enabled_git_access_protocol,
+ :gravatar_enabled,
+ :help_page_hide_commercial_content,
+ :help_page_support_url,
+ :help_page_text,
+ :home_page_url,
+ :housekeeping_bitmaps_enabled,
+ :housekeeping_enabled,
+ :housekeeping_full_repack_period,
+ :housekeeping_gc_period,
+ :housekeeping_incremental_repack_period,
+ :html_emails_enabled,
+ :import_sources,
+ :koding_enabled,
+ :koding_url,
+ :max_artifacts_size,
+ :max_attachment_size,
+ :max_pages_size,
+ :metrics_enabled,
+ :metrics_host,
+ :metrics_method_call_threshold,
+ :metrics_packet_size,
+ :metrics_pool_size,
+ :metrics_port,
+ :metrics_sample_interval,
+ :metrics_timeout,
+ :password_authentication_enabled,
+ :performance_bar_allowed_group_id,
+ :performance_bar_enabled,
+ :plantuml_enabled,
+ :plantuml_url,
+ :polling_interval_multiplier,
+ :prometheus_metrics_enabled,
+ :recaptcha_enabled,
+ :recaptcha_private_key,
+ :recaptcha_site_key,
+ :repository_checks_enabled,
+ :repository_storages,
+ :require_two_factor_authentication,
+ :restricted_visibility_levels,
+ :send_user_confirmation_email,
+ :sentry_dsn,
+ :sentry_enabled,
+ :session_expire_delay,
+ :shared_runners_enabled,
+ :shared_runners_text,
+ :sidekiq_throttling_enabled,
+ :sidekiq_throttling_factor,
+ :sidekiq_throttling_queues,
+ :sign_in_text,
+ :signup_enabled,
+ :terminal_max_session_time,
+ :two_factor_grace_period,
+ :unique_ips_limit_enabled,
+ :unique_ips_limit_per_user,
+ :unique_ips_limit_time_window,
+ :usage_ping_enabled,
+ :user_default_external,
+ :user_oauth_applications,
+ :version_check_enabled
+ ]
+ end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index d08e346d605..69220a1c0f6 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -113,6 +113,10 @@ module CommitsHelper
commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
end
+ def commit_signature_badge_classes(additional_classes)
+ %w(btn status-box gpg-status-box) + Array(additional_classes)
+ end
+
protected
# Private: Returns a link to a person. If the person has a matching user and
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 926502bf239..91ddd73fac1 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -15,7 +15,7 @@ module DiffHelper
def diff_view
@diff_view ||= begin
diff_views = %w(inline parallel)
- diff_view = cookies[:diff_view]
+ diff_view = params[:view] || cookies[:diff_view]
diff_view = diff_views.first unless diff_views.include?(diff_view)
diff_view.to_sym
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 425af547330..f4fad7150e8 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -354,4 +354,14 @@ module IssuablesHelper
params[:format] = :json if issuable.is_a?(Issue)
end
end
+
+ def issuable_sidebar_options(issuable, can_edit_issuable)
+ {
+ endpoint: "#{issuable_json_path(issuable)}?basic=true",
+ editable: can_edit_issuable,
+ currentUser: current_user.as_json(only: [:username, :id, :name], methods: :avatar_url),
+ rootPath: root_path,
+ fullPath: @project.full_path
+ }
+ end
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 8f4e39b8b23..e857e837c16 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -134,4 +134,14 @@ module NotesHelper
can?(current_user, :create_note, @project)
end
end
+
+ def initial_notes_data(autocomplete)
+ {
+ notesUrl: notes_url,
+ notesIds: @notes.map(&:id),
+ now: Time.now.to_i,
+ diffView: diff_view,
+ autocomplete: autocomplete
+ }
+ end
end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 256cbcd73a1..c401030e34a 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -14,7 +14,7 @@ module Emails
end
def new_ssh_key_email(key_id)
- @key = Key.find_by_id(key_id)
+ @key = Key.find_by(id: key_id)
return unless @key
@@ -22,5 +22,15 @@ module Emails
@target_url = user_url(@user)
mail(to: @user.notification_email, subject: subject("SSH key was added to your account"))
end
+
+ def new_gpg_key_email(gpg_key_id)
+ @gpg_key = GpgKey.find_by(id: gpg_key_id)
+
+ return unless @gpg_key
+
+ @current_user = @user = @gpg_key.user
+ @target_url = user_url(@user)
+ mail(to: @user.notification_email, subject: subject("GPG key was added to your account"))
+ end
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 898ce45f60e..bd7c4cd45ea 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -315,7 +315,9 @@ class ApplicationSetting < ActiveRecord::Base
Array(read_attribute(:repository_storages))
end
+ # DEPRECATED
# repository_storage is still required in the API. Remove in 9.0
+ # Still used in API v3
def repository_storage
repository_storages.first
end
diff --git a/app/models/chat_team.rb b/app/models/chat_team.rb
index c52b6f15913..25ecf2d5937 100644
--- a/app/models/chat_team.rb
+++ b/app/models/chat_team.rb
@@ -3,4 +3,13 @@ class ChatTeam < ActiveRecord::Base
validates :namespace, uniqueness: true
belongs_to :namespace
+
+ def remove_mattermost_team(current_user)
+ Mattermost::Team.new(current_user).destroy(team_id: team_id)
+ rescue Mattermost::ClientError => e
+ # Either the group is not found, or the user doesn't have the proper
+ # access on the mattermost instance. In the first case, we're done either way
+ # in the latter case, we can't recover by retrying, so we just log what happened
+ Rails.logger.error("Mattermost team deletion failed: #{e}")
+ end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 416a2a33378..8be2dee6479 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -219,6 +219,7 @@ module Ci
variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group
variables += secret_variables(environment: environment)
variables += trigger_request.user_variables if trigger_request
+ variables += pipeline.variables.map(&:to_runner_variable)
variables += pipeline.pipeline_schedule.job_variables if pipeline.pipeline_schedule
variables += persisted_environment_variables if environment
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index e5b615a7cc0..d2abcf30034 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -15,6 +15,7 @@ module Ci
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
has_many :builds, foreign_key: :commit_id
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
+ has_many :variables, class_name: 'Ci::PipelineVariable'
# Merge requests for which the current pipeline is running against
# the merge request's latest commit.
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
new file mode 100644
index 00000000000..00b419c3efa
--- /dev/null
+++ b/app/models/ci/pipeline_variable.rb
@@ -0,0 +1,10 @@
+module Ci
+ class PipelineVariable < ActiveRecord::Base
+ extend Ci::Model
+ include HasVariable
+
+ belongs_to :pipeline
+
+ validates :key, uniqueness: { scope: :pipeline_id }
+ end
+end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 1e19f00106a..7940733f557 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -234,6 +234,14 @@ class Commit
@statuses[ref] = pipelines.latest_status(ref)
end
+ def signature
+ return @signature if defined?(@signature)
+
+ @signature = gpg_commit.signature
+ end
+
+ delegate :has_signature?, to: :gpg_commit
+
def revert_branch_name
"revert-#{short_id}"
end
@@ -382,4 +390,8 @@ class Commit
def merged_merge_request_no_cache(user)
MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit?
end
+
+ def gpg_commit
+ @gpg_commit ||= Gitlab::Gpg::Commit.new(self)
+ end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 13fe9d09c69..935ffe343ff 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -71,9 +71,8 @@ module Issuable
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
- scope :opened, -> { with_state(:opened, :reopened) }
+ scope :opened, -> { with_state(:opened) }
scope :only_opened, -> { with_state(:opened) }
- scope :only_reopened, -> { with_state(:reopened) }
scope :closed, -> { with_state(:closed) }
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
@@ -234,7 +233,7 @@ module Issuable
end
def open?
- opened? || reopened?
+ opened?
end
def user_notes_count
diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb
new file mode 100644
index 00000000000..3df60ddc950
--- /dev/null
+++ b/app/models/gpg_key.rb
@@ -0,0 +1,107 @@
+class GpgKey < ActiveRecord::Base
+ KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----'.freeze
+ KEY_SUFFIX = '-----END PGP PUBLIC KEY BLOCK-----'.freeze
+
+ include ShaAttribute
+
+ sha_attribute :primary_keyid
+ sha_attribute :fingerprint
+
+ belongs_to :user
+ has_many :gpg_signatures
+
+ validates :user, presence: true
+
+ validates :key,
+ presence: true,
+ uniqueness: true,
+ format: {
+ with: /\A#{KEY_PREFIX}((?!#{KEY_PREFIX})(?!#{KEY_SUFFIX}).)+#{KEY_SUFFIX}\Z/m,
+ message: "is invalid. A valid public GPG key begins with '#{KEY_PREFIX}' and ends with '#{KEY_SUFFIX}'"
+ }
+
+ validates :fingerprint,
+ presence: true,
+ uniqueness: true,
+ # only validate when the `key` is valid, as we don't want the user to show
+ # the error about the fingerprint
+ unless: -> { errors.has_key?(:key) }
+
+ validates :primary_keyid,
+ presence: true,
+ uniqueness: true,
+ # only validate when the `key` is valid, as we don't want the user to show
+ # the error about the fingerprint
+ unless: -> { errors.has_key?(:key) }
+
+ before_validation :extract_fingerprint, :extract_primary_keyid
+ after_commit :update_invalid_gpg_signatures, on: :create
+ after_commit :notify_user, on: :create
+
+ def primary_keyid
+ super&.upcase
+ end
+
+ def fingerprint
+ super&.upcase
+ end
+
+ def key=(value)
+ super(value&.strip)
+ end
+
+ def user_infos
+ @user_infos ||= Gitlab::Gpg.user_infos_from_key(key)
+ end
+
+ def verified_user_infos
+ user_infos.select do |user_info|
+ user_info[:email] == user.email
+ end
+ end
+
+ def emails_with_verified_status
+ user_infos.map do |user_info|
+ [
+ user_info[:email],
+ user_info[:email] == user.email
+ ]
+ end.to_h
+ end
+
+ def verified?
+ emails_with_verified_status.any? { |_email, verified| verified }
+ end
+
+ def update_invalid_gpg_signatures
+ InvalidGpgSignatureUpdateWorker.perform_async(self.id)
+ end
+
+ def revoke
+ GpgSignature.where(gpg_key: self, valid_signature: true).update_all(
+ gpg_key_id: nil,
+ valid_signature: false,
+ updated_at: Time.zone.now
+ )
+
+ destroy
+ end
+
+ private
+
+ def extract_fingerprint
+ # we can assume that the result only contains one item as the validation
+ # only allows one key
+ self.fingerprint = Gitlab::Gpg.fingerprints_from_key(key).first
+ end
+
+ def extract_primary_keyid
+ # we can assume that the result only contains one item as the validation
+ # only allows one key
+ self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first
+ end
+
+ def notify_user
+ NotificationService.new.new_gpg_key(self)
+ end
+end
diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb
new file mode 100644
index 00000000000..1ac0e123ff1
--- /dev/null
+++ b/app/models/gpg_signature.rb
@@ -0,0 +1,21 @@
+class GpgSignature < ActiveRecord::Base
+ include ShaAttribute
+
+ sha_attribute :commit_sha
+ sha_attribute :gpg_key_primary_keyid
+
+ belongs_to :project
+ belongs_to :gpg_key
+
+ validates :commit_sha, presence: true
+ validates :project_id, presence: true
+ validates :gpg_key_primary_keyid, presence: true
+
+ def gpg_key_primary_keyid
+ super&.upcase
+ end
+
+ def commit
+ project.commit(commit_sha)
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 400bb55d2f0..1c948c8957e 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -62,15 +62,14 @@ class Issue < ActiveRecord::Base
state_machine :state, initial: :opened do
event :close do
- transition [:reopened, :opened] => :closed
+ transition [:opened] => :closed
end
event :reopen do
- transition closed: :reopened
+ transition closed: :opened
end
state :opened
- state :reopened
state :closed
before_transition any => :closed do |issue|
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index a910099b4c1..81e0776e79c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -42,23 +42,23 @@ class MergeRequest < ActiveRecord::Base
state_machine :state, initial: :opened do
event :close do
- transition [:reopened, :opened] => :closed
+ transition [:opened] => :closed
end
event :mark_as_merged do
- transition [:reopened, :opened, :locked] => :merged
+ transition [:opened, :locked] => :merged
end
event :reopen do
- transition closed: :reopened
+ transition closed: :opened
end
event :lock_mr do
- transition [:reopened, :opened] => :locked
+ transition [:opened] => :locked
end
event :unlock_mr do
- transition locked: :reopened
+ transition locked: :opened
end
after_transition any => :locked do |merge_request, transition|
@@ -72,7 +72,6 @@ class MergeRequest < ActiveRecord::Base
end
state :opened
- state :reopened
state :closed
state :merged
state :locked
@@ -368,7 +367,7 @@ class MergeRequest < ActiveRecord::Base
errors.add :branch_conflict, "You can not use same project/branch for source and target"
end
- if opened? || reopened?
+ if opened?
similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
if similar_mrs.any?
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index f6cade9c290..c93f1632652 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -114,7 +114,7 @@ class DroneCiService < CiService
end
def merge_request_valid?(data)
- %w(opened reopened).include?(data[:object_attributes][:state]) &&
+ data[:object_attributes][:state] == 'opened' &&
data[:object_attributes][:merge_status] == 'unchecked'
end
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 37f2c96a22f..2aa19443198 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -160,7 +160,10 @@ class JiraService < IssueTrackerService
def test(_)
result = test_settings
- { success: result.present?, result: result }
+ success = result.present?
+ result = @error if @error && !success
+
+ { success: success, result: result }
end
# JIRA does not need test data.
@@ -288,7 +291,8 @@ class JiraService < IssueTrackerService
yield
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => e
- Rails.logger.info "#{self.class.name} Send message ERROR: #{client_url} - #{e.message}"
+ @error = e.message
+ Rails.logger.info "#{self.class.name} Send message ERROR: #{client_url} - #{@error}"
nil
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index d27eeff9fb4..50b7a477904 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -457,10 +457,6 @@ class Repository
nil
end
- def blob_by_oid(oid)
- Gitlab::Git::Blob.raw(self, oid)
- end
-
def root_ref
if raw_repository
raw_repository.root_ref
diff --git a/app/models/user.rb b/app/models/user.rb
index c26be6d05a2..6e66c587a1f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -76,6 +76,7 @@ class User < ActiveRecord::Base
where(type.not_eq('DeployKey').or(type.eq(nil)))
end, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :gpg_keys
has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :personal_access_tokens, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -157,6 +158,7 @@ class User < ActiveRecord::Base
before_save :ensure_authentication_token, :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: :external_changed?
after_save :ensure_namespace_correct
+ after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
after_initialize :set_projects_limit
after_destroy :post_destroy_hook
@@ -512,6 +514,10 @@ class User < ActiveRecord::Base
end
end
+ def update_invalid_gpg_signatures
+ gpg_keys.each(&:update_invalid_gpg_signatures)
+ end
+
# Returns the groups a user has access to
def authorized_groups
union = Gitlab::SQL::Union
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 224eb3cd4d0..148998bc9be 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -1,4 +1,6 @@
class WikiPage
+ PageChangedError = Class.new(StandardError)
+
include ActiveModel::Validations
include ActiveModel::Conversion
include StaticModel
@@ -136,6 +138,10 @@ class WikiPage
versions.first
end
+ def last_commit_sha
+ commit&.sha
+ end
+
# Returns the Date that this latest version was
# created on.
def created_at
@@ -182,17 +188,22 @@ class WikiPage
# Updates an existing Wiki Page, creating a new version.
#
- # new_content - The raw markup content to replace the existing.
- # format - Optional symbol representing the content format.
- # See ProjectWiki::MARKUPS Hash for available formats.
- # message - Optional commit message to set on the new version.
+ # new_content - The raw markup content to replace the existing.
+ # format - Optional symbol representing the content format.
+ # See ProjectWiki::MARKUPS Hash for available formats.
+ # message - Optional commit message to set on the new version.
+ # last_commit_sha - Optional last commit sha to validate the page unchanged.
#
# Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful.
- def update(new_content = "", format = :markdown, message = nil)
+ def update(new_content, format: :markdown, message: nil, last_commit_sha: nil)
@attributes[:content] = new_content
@attributes[:format] = format
+ if last_commit_sha && last_commit_sha != self.last_commit_sha
+ raise PageChangedError.new("You are attempting to update a page that has changed since you started editing it.")
+ end
+
save :update_page, @page, content, format, message
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 21e2ef153de..884b681ff81 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -21,14 +21,22 @@ module Ci
return result if result
- Ci::Pipeline.transaction do
- update_merge_requests_head_pipeline if pipeline.save
+ begin
+ Ci::Pipeline.transaction do
+ pipeline.save!
- Ci::CreatePipelineStagesService
- .new(project, current_user)
- .execute(pipeline)
+ yield(pipeline) if block_given?
+
+ Ci::CreatePipelineStagesService
+ .new(project, current_user)
+ .execute(pipeline)
+ end
+ rescue ActiveRecord::RecordInvalid => e
+ return error("Failed to persist the pipeline: #{e}")
end
+ update_merge_requests_head_pipeline
+
cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
pipeline_created_counter.increment(source: source)
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
index a43d0e4593c..b2aa457bbd5 100644
--- a/app/services/ci/create_trigger_request_service.rb
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -1,3 +1,8 @@
+# This class is deprecated because we're closing Ci::TriggerRequest.
+# New class is PipelineTriggerService (app/services/ci/pipeline_trigger_service.rb)
+# which is integrated with Ci::PipelineVariable instaed of Ci::TriggerRequest.
+# We remove this class after we removed v1 and v3 API. This class is still being
+# referred by such legacy code.
module Ci
module CreateTriggerRequestService
Result = Struct.new(:trigger_request, :pipeline)
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
new file mode 100644
index 00000000000..1e5ad28ba57
--- /dev/null
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -0,0 +1,44 @@
+module Ci
+ class PipelineTriggerService < BaseService
+ def execute
+ if trigger_from_token
+ create_pipeline_from_trigger(trigger_from_token)
+ end
+ end
+
+ private
+
+ def create_pipeline_from_trigger(trigger)
+ # this check is to not leak the presence of the project if user cannot read it
+ return unless trigger.project == project
+
+ pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref])
+ .execute(:trigger, ignore_skip_ci: true) do |pipeline|
+ trigger.trigger_requests.create!(pipeline: pipeline)
+ create_pipeline_variables!(pipeline)
+ end
+
+ if pipeline.persisted?
+ success(pipeline: pipeline)
+ else
+ error(pipeline.errors.messages, 400)
+ end
+ end
+
+ def trigger_from_token
+ return @trigger if defined?(@trigger)
+
+ @trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ end
+
+ def create_pipeline_variables!(pipeline)
+ return unless params[:variables]
+
+ variables = params[:variables].map do |key, value|
+ { key: key, value: value }
+ end
+
+ pipeline.variables.create!(variables)
+ end
+ end
+end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 20d1fb29289..bb7680c5054 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -56,6 +56,8 @@ class GitPushService < BaseService
perform_housekeeping
update_caches
+
+ update_signatures
end
def update_gitattributes
@@ -80,6 +82,12 @@ class GitPushService < BaseService
ProjectCacheWorker.perform_async(@project.id, types, [:commit_count, :repository_size])
end
+ def update_signatures
+ @push_commits.each do |commit|
+ CreateGpgSignatureWorker.perform_async(commit.sha, @project.id)
+ end
+ end
+
# Schedules processing of commit messages.
def process_commit_messages
default = is_default_branch?
diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb
index 80c51cb5a72..f565612a89d 100644
--- a/app/services/groups/destroy_service.rb
+++ b/app/services/groups/destroy_service.rb
@@ -21,6 +21,8 @@ module Groups
DestroyService.new(group, current_user).execute
end
+ group.chat_team&.remove_mattermost_team(current_user)
+
group.really_destroy!
end
end
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index 73b2e85cba3..35de4337b15 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -5,7 +5,7 @@ module Issues
if issue.reopen
event_service.reopen_issue(issue, current_user)
- create_note(issue)
+ create_note(issue, 'reopened')
notification_service.reopen_issue(issue, current_user)
execute_hooks(issue, 'reopen')
invalidate_cache_counts(issue, users: issue.assignees)
@@ -16,8 +16,8 @@ module Issues
private
- def create_note(issue)
- SystemNoteService.change_status(issue, issue.project, current_user, issue.state, nil)
+ def create_note(issue, state = issue.state)
+ SystemNoteService.change_status(issue, issue.project, current_user, state, nil)
end
end
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 3542a41ac83..35ccff26262 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -1,7 +1,7 @@
module MergeRequests
class BaseService < ::IssuableBaseService
- def create_note(merge_request)
- SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
+ def create_note(merge_request, state = merge_request.state)
+ SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, state, nil)
end
def create_title_change_note(issuable, old_title)
@@ -44,7 +44,7 @@ module MergeRequests
end
# Returns all origin and fork merge requests from `@project` satisfying passed arguments.
- def merge_requests_for(source_branch, mr_states: [:opened, :reopened])
+ def merge_requests_for(source_branch, mr_states: [:opened])
MergeRequest
.with_state(mr_states)
.where(source_branch: source_branch, source_project_id: @project.id)
diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb
index 52f6d511f98..b9c65be36ec 100644
--- a/app/services/merge_requests/reopen_service.rb
+++ b/app/services/merge_requests/reopen_service.rb
@@ -5,7 +5,7 @@ module MergeRequests
if merge_request.reopen
event_service.reopen_mr(merge_request, current_user)
- create_note(merge_request)
+ create_note(merge_request, 'reopened')
notification_service.reopen_mr(merge_request, current_user)
execute_hooks(merge_request, 'reopen')
merge_request.reload_diff(current_user)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 3a98a5f6b64..b94921d2a08 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -17,6 +17,16 @@ class NotificationService
end
end
+ # Always notify the user about gpg key added
+ #
+ # This is a security email so it will be sent even if the user user disabled
+ # notifications
+ def new_gpg_key(gpg_key)
+ if gpg_key.user
+ mailer.new_gpg_key_email(gpg_key.id).deliver_later
+ end
+ end
+
# Always notify user about email added to profile
def new_email(email)
if email.user
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index a5110a23cad..27c3ba197ac 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -44,7 +44,7 @@ class WebHookService
http_status: response.code,
message: response.to_s
}
- rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
+ rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout => e
log_execution(
trigger: hook_name,
url: hook.url,
diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb
index 8f6a50da838..c628e6781af 100644
--- a/app/services/wiki_pages/update_service.rb
+++ b/app/services/wiki_pages/update_service.rb
@@ -1,7 +1,7 @@
module WikiPages
class UpdateService < WikiPages::BaseService
def execute(page)
- if page.update(@params[:content], @params[:format], @params[:message])
+ if page.update(@params[:content], format: @params[:format], message: @params[:message], last_commit_sha: @params[:last_commit_sha])
execute_hooks(page, 'update')
end
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index 061f8991b11..93827d6a1ab 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -6,6 +6,7 @@
.col-sm-10
= f.text_field :name, class: 'form-control'
= doorkeeper_errors_for application, :name
+
= content_tag :div, class: 'form-group' do
= f.label :redirect_uri, class: 'col-sm-2 control-label'
.col-sm-10
@@ -19,6 +20,13 @@
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
+ = content_tag :div, class: 'form-group' do
+ = f.label :trusted, class: 'col-sm-2 control-label'
+ .col-sm-10
+ = f.check_box :trusted
+ %span.help-block
+ Trusted applications are automatically authorized on GitLab OAuth flow.
+
.form-group
= f.label :scopes, class: 'col-sm-2 control-label'
.col-sm-10
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index eb4293c7e37..94d33fa6489 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -11,6 +11,7 @@
%th Name
%th Callback URL
%th Clients
+ %th Trusted
%th
%th
%tbody.oauth-applications
@@ -19,5 +20,6 @@
%td= link_to application.name, admin_application_path(application)
%td= application.redirect_uri
%td= application.access_tokens.map(&:resource_owner_id).uniq.count
+ %td= application.trusted? ? 'Y': 'N'
%td= link_to 'Edit', edit_admin_application_path(application), class: 'btn btn-link'
%td= render 'delete_form', application: application
diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml
index 14683cc66e9..5125aa21b06 100644
--- a/app/views/admin/applications/show.html.haml
+++ b/app/views/admin/applications/show.html.haml
@@ -23,6 +23,12 @@
%div
%span.monospace= uri
+ %tr
+ %td
+ Trusted
+ %td
+ = @application.trusted? ? 'Y' : 'N'
+
= render "shared/tokens/scopes_list", token: @application
.form-actions
diff --git a/app/views/groups/_shared_projects.html.haml b/app/views/groups/_shared_projects.html.haml
deleted file mode 100644
index b1694c919d0..00000000000
--- a/app/views/groups/_shared_projects.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: false
diff --git a/app/views/layouts/help.html.haml b/app/views/layouts/help.html.haml
index 224b24befbe..78927bfffcd 100644
--- a/app/views/layouts/help.html.haml
+++ b/app/views/layouts/help.html.haml
@@ -1,3 +1,4 @@
+- @breadcrumb_title = "Help"
- page_title "Help"
- header_title "Help", help_path
diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/_new_profile_sidebar.html.haml
index 239e6b949e2..6bbd569583e 100644
--- a/app/views/layouts/nav/_new_profile_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_profile_sidebar.html.haml
@@ -47,6 +47,10 @@
= link_to profile_keys_path, title: 'SSH Keys' do
%span
SSH Keys
+ = nav_link(controller: :gpg_keys) do
+ = link_to profile_gpg_keys_path, title: 'GPG Keys' do
+ %span
+ GPG Keys
= nav_link(controller: :preferences) do
= link_to profile_preferences_path, title: 'Preferences' do
%span
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 424905ea890..26d9640e98a 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -43,6 +43,10 @@
= link_to profile_keys_path, title: 'SSH Keys' do
%span
SSH Keys
+ = nav_link(controller: :gpg_keys) do
+ = link_to profile_gpg_keys_path, title: 'GPG Keys' do
+ %span
+ GPG Keys
= nav_link(controller: :preferences) do
= link_to profile_preferences_path, title: 'Preferences' do
%span
diff --git a/app/views/notify/new_gpg_key_email.html.haml b/app/views/notify/new_gpg_key_email.html.haml
new file mode 100644
index 00000000000..4b9350c4e88
--- /dev/null
+++ b/app/views/notify/new_gpg_key_email.html.haml
@@ -0,0 +1,10 @@
+%p
+ Hi #{@user.name}!
+%p
+ A new GPG key was added to your account:
+%p
+ Fingerprint:
+ %code= @gpg_key.fingerprint
+%p
+ If this key was added in error, you can remove it under
+ = link_to "GPG Keys", profile_gpg_keys_url
diff --git a/app/views/notify/new_gpg_key_email.text.erb b/app/views/notify/new_gpg_key_email.text.erb
new file mode 100644
index 00000000000..80b5a1fd7ff
--- /dev/null
+++ b/app/views/notify/new_gpg_key_email.text.erb
@@ -0,0 +1,7 @@
+Hi <%= @user.name %>!
+
+A new GPG key was added to your account:
+
+Fingerprint: <%= @gpg_key.fingerprint %>
+
+If this key was added in error, you can remove it at <%= profile_gpg_keys_url %>
diff --git a/app/views/profiles/gpg_keys/_email_with_badge.html.haml b/app/views/profiles/gpg_keys/_email_with_badge.html.haml
new file mode 100644
index 00000000000..5f7844584e1
--- /dev/null
+++ b/app/views/profiles/gpg_keys/_email_with_badge.html.haml
@@ -0,0 +1,8 @@
+- css_classes = %w(label label-verification-status)
+- css_classes << (verified ? 'verified': 'unverified')
+- text = verified ? 'Verified' : 'Unverified'
+
+.gpg-email-badge
+ .gpg-email-badge-email= email
+ %div{ class: css_classes }
+ = text
diff --git a/app/views/profiles/gpg_keys/_form.html.haml b/app/views/profiles/gpg_keys/_form.html.haml
new file mode 100644
index 00000000000..3fcf563d970
--- /dev/null
+++ b/app/views/profiles/gpg_keys/_form.html.haml
@@ -0,0 +1,10 @@
+%div
+ = form_for [:profile, @gpg_key], html: { class: 'js-requires-input' } do |f|
+ = form_errors(@gpg_key)
+
+ .form-group
+ = f.label :key, class: 'label-light'
+ = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: "Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'."
+
+ .prepend-top-default
+ = f.submit 'Add key', class: "btn btn-create"
diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml
new file mode 100644
index 00000000000..b04981f90e3
--- /dev/null
+++ b/app/views/profiles/gpg_keys/_key.html.haml
@@ -0,0 +1,18 @@
+%li.key-list-item
+ .pull-left.append-right-10
+ = icon 'key', class: "settings-list-icon hidden-xs"
+ .key-list-item-info
+ - key.emails_with_verified_status.map do |email, verified|
+ = render partial: 'email_with_badge', locals: { email: email, verified: verified }
+
+ .description
+ %code= key.fingerprint
+ .pull-right
+ %span.key-created-at
+ created #{time_ago_with_tooltip(key.created_at)}
+ = link_to profile_gpg_key_path(key), data: { confirm: 'Are you sure? Removing this GPG key does not affect already signed commits.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
+ %span.sr-only Remove
+ = icon('trash')
+ = link_to revoke_profile_gpg_key_path(key), data: { confirm: 'Are you sure? All commits that were signed with this GPG key will be unverified.' }, method: :put, class: "btn btn-danger prepend-left-10" do
+ %span.sr-only Revoke
+ Revoke
diff --git a/app/views/profiles/gpg_keys/_key_table.html.haml b/app/views/profiles/gpg_keys/_key_table.html.haml
new file mode 100644
index 00000000000..cabb92c5a24
--- /dev/null
+++ b/app/views/profiles/gpg_keys/_key_table.html.haml
@@ -0,0 +1,11 @@
+- is_admin = local_assigns.fetch(:admin, false)
+
+- if @gpg_keys.any?
+ %ul.well-list
+ = render partial: 'profiles/gpg_keys/key', collection: @gpg_keys, locals: { is_admin: is_admin }
+- else
+ %p.settings-message.text-center
+ - if is_admin
+ There are no GPG keys associated with this account.
+ - else
+ There are no GPG keys with access to your account.
diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml
new file mode 100644
index 00000000000..8331daeeb75
--- /dev/null
+++ b/app/views/profiles/gpg_keys/index.html.haml
@@ -0,0 +1,21 @@
+- page_title "GPG Keys"
+= render 'profiles/head'
+
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ GPG keys allow you to verify signed commits.
+ .col-lg-9
+ %h5.prepend-top-0
+ Add a GPG key
+ %p.profile-settings-content
+ Before you can add a GPG key you need to
+ = link_to 'generate it.', help_page_path('workflow/gpg_signed_commits/index.md')
+ = render 'form'
+ %hr
+ %h5
+ Your GPG keys (#{@gpg_keys.count})
+ .append-bottom-default
+ = render 'key_table'
diff --git a/app/views/projects/blob/viewers/_image.html.haml b/app/views/projects/blob/viewers/_image.html.haml
index 1650aa8197f..5fd22a59217 100644
--- a/app/views/projects/blob/viewers/_image.html.haml
+++ b/app/views/projects/blob/viewers/_image.html.haml
@@ -1,2 +1,2 @@
.file-content.image_file
- %img{ 'data-src': blob_raw_url, alt: viewer.blob.name }
+ = image_tag(blob_raw_url, alt: viewer.blob.name)
diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml
new file mode 100644
index 00000000000..22674b671c9
--- /dev/null
+++ b/app/views/projects/commit/_ajax_signature.html.haml
@@ -0,0 +1,3 @@
+- if commit.has_signature?
+ %button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
+ %i.fa.fa-spinner.fa-spin
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 45109f2c58b..419fbe99af8 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,5 +1,6 @@
.page-content-header
.header-main-content
+ = render partial: 'signature', object: @commit.signature
%strong
#{ s_('CommitBoxTitle|Commit') }
%span.commit-sha= @commit.short_id
diff --git a/app/views/projects/commit/_invalid_signature_badge.html.haml b/app/views/projects/commit/_invalid_signature_badge.html.haml
new file mode 100644
index 00000000000..3a73aae9d95
--- /dev/null
+++ b/app/views/projects/commit/_invalid_signature_badge.html.haml
@@ -0,0 +1,9 @@
+- title = capture do
+ .gpg-popover-icon.invalid
+ = render 'shared/icons/icon_status_notfound_borderless.svg'
+ %div
+ This commit was signed with an <strong>unverified</strong> signature.
+
+- locals = { signature: signature, title: title, label: 'Unverified', css_classes: ['invalid'] }
+
+= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml
new file mode 100644
index 00000000000..60fa52557ef
--- /dev/null
+++ b/app/views/projects/commit/_signature.html.haml
@@ -0,0 +1,5 @@
+- if signature
+ - if signature.valid_signature?
+ = render partial: 'projects/commit/valid_signature_badge', locals: { signature: signature }
+ - else
+ = render partial: 'projects/commit/invalid_signature_badge', locals: { signature: signature }
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
new file mode 100644
index 00000000000..66f00eb5507
--- /dev/null
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -0,0 +1,18 @@
+- css_classes = commit_signature_badge_classes(css_classes)
+
+- title = capture do
+ .gpg-popover-status
+ = title
+
+- content = capture do
+ .clearfix
+ = content
+
+ GPG Key ID:
+ %span.monospace= signature.gpg_key_primary_keyid
+
+
+ = link_to('Learn more about signing commits', help_page_path('workflow/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
+
+%button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } }
+ = label
diff --git a/app/views/projects/commit/_valid_signature_badge.html.haml b/app/views/projects/commit/_valid_signature_badge.html.haml
new file mode 100644
index 00000000000..db1a41bbf64
--- /dev/null
+++ b/app/views/projects/commit/_valid_signature_badge.html.haml
@@ -0,0 +1,32 @@
+- title = capture do
+ .gpg-popover-icon.valid
+ = render 'shared/icons/icon_status_success_borderless.svg'
+ %div
+ This commit was signed with a <strong>verified</strong> signature.
+
+- content = capture do
+ - gpg_key = signature.gpg_key
+ - user = gpg_key&.user
+ - user_name = signature.gpg_key_user_name
+ - user_email = signature.gpg_key_user_email
+
+ - if user
+ = link_to user_path(user), class: 'gpg-popover-user-link' do
+ %div
+ = user_avatar_without_link(user: user, size: 32)
+
+ %div
+ %strong= gpg_key.user.name
+ %div @#{gpg_key.user.username}
+ - else
+ = mail_to user_email do
+ %div
+ = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
+
+ %div
+ %strong= user_name
+ %div= user_email
+
+- locals = { signature: signature, title: title, content: content, label: 'Verified', css_classes: ['valid'] }
+
+= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 1033bad0d49..12b73ecdf13 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -9,7 +9,7 @@
- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
- %li.commit.flex-list.js-toggle-container{ id: "commit-#{commit.short_id}" }
+ %li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
.avatar-cell.hidden-xs
= author_avatar(commit, size: 36)
@@ -36,9 +36,15 @@
#{ commit_text.html_safe }
- .commit-actions.flex-row.hidden-xs
+ .commit-actions.hidden-xs
- if commit.status(ref)
= render_commit_status(commit, ref: ref)
+
+ - if request.xhr?
+ = render partial: 'projects/commit/signature', object: commit.signature
+ - else
+ = render partial: 'projects/commit/ajax_signature', locals: { commit: commit }
+
= link_to commit.short_id, project_commit_path(project, commit), class: "commit-sha btn btn-transparent"
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"))
= link_to_browse_code(project, commit)
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index c764e35dd2a..d14897428d0 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -7,7 +7,7 @@
%span.commits-count= n_("%d commit", "%d commits", commits.count) % commits.count
%li.commits-row{ data: { day: day } }
- %ul.content-list.commit-list
+ %ul.content-list.commit-list.flex-list
= render partial: 'projects/commits/commit', collection: commits, locals: { project: project, ref: ref }
- if hidden > 0
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 844ebb65148..bd2d900997e 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -29,7 +29,7 @@
= link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
.control
- = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form') do
+ = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form', data: { 'signatures-path' => namespace_project_signatures_path }) do
= search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
.control
= link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do
diff --git a/app/views/projects/diffs/viewers/_image.html.haml b/app/views/projects/diffs/viewers/_image.html.haml
index 05877ceed3d..aa004a739d7 100644
--- a/app/views/projects/diffs/viewers/_image.html.haml
+++ b/app/views/projects/diffs/viewers/_image.html.haml
@@ -8,7 +8,7 @@
.image
%span.wrap
.frame{ class: (diff_file.deleted_file? ? 'deleted' : 'added') }
- %img{ 'data-src': blob_raw_path, alt: diff_file.file_path }
+ = image_tag(blob_raw_path, alt: diff_file.file_path)
%p.image-info= number_to_human_size(blob.size)
- else
.image
@@ -16,7 +16,7 @@
%span.wrap
.frame.deleted
%a{ href: project_blob_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) }
- %img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
+ = image_tag(old_blob_raw_path, alt: diff_file.old_path)
%p.image-info.hide
%span.meta-filesize= number_to_human_size(old_blob.size)
|
@@ -28,7 +28,7 @@
%span.wrap
.frame.added
%a{ href: project_blob_path(@project, tree_join(diff_file.content_sha, diff_file.new_path)) }
- %img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
+ = image_tag(blob_raw_path, alt: diff_file.new_path)
%p.image-info.hide
%span.meta-filesize= number_to_human_size(blob.size)
|
@@ -41,10 +41,10 @@
.swipe.view.hide
.swipe-frame
.frame.deleted
- %img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
+ = image_tag(old_blob_raw_path, alt: diff_file.old_path)
.swipe-wrap
.frame.added
- %img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
+ = image_tag(blob_raw_path, alt: diff_file.new_path)
%span.swipe-bar
%span.top-handle
%span.bottom-handle
@@ -52,9 +52,9 @@
.onion-skin.view.hide
.onion-skin-frame
.frame.deleted
- %img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
+ = image_tag(old_blob_raw_path, alt: diff_file.old_path)
.frame.added
- %img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
+ = image_tag(blob_raw_path, alt: diff_file.new_path)
.controls
.transparent
.drag-track
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index fc6b7a33943..adb8d5aaecb 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -4,6 +4,8 @@
= form_errors(@page)
= f.hidden_field :title, value: @page.title
+ - if @page.persisted?
+ = f.hidden_field :last_commit_sha, value: @page.last_commit_sha
.form-group
.col-sm-12= f.label :format, class: 'control-label-full-width'
.col-sm-12
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index df0ec14eb3b..8fd60216536 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,6 +1,12 @@
- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
- page_title "Edit", @page.title.capitalize, "Wiki"
+- if @conflict
+ .alert.alert-danger
+ Someone edited the page the same time you did. Please check out
+ = link_to "the page", project_wiki_path(@project, @page), target: "_blank"
+ and make sure your changes will not unintentionally remove theirs.
+
.wiki-page-header.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 75704eda361..b4843eafdb7 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -20,11 +20,3 @@
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' }
.input-group-btn
= clipboard_button(target: '#project_clone', title: _("Copy URL to clipboard"), class: "btn-default btn-clipboard")
-
-:javascript
- $('ul.clone-options-dropdown a').on('click',function(e){
- e.preventDefault();
- var $this = $(this);
- $('a.clone-dropdown-btn span').text($this.text());
- $('#project_clone').val($this.attr('href'));
- });
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 2f776a17f45..8ded7440de3 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -76,11 +76,3 @@
= link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do
%span.sr-only Delete
= icon('trash-o')
-
- - if current_user
- - if can_subscribe_to_label_in_different_levels?(label)
- :javascript
- new gl.GroupLabelSubscription('##{dom_id(label)} .label-subscription');
- - else
- :javascript
- new gl.ProjectLabelSubscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
index b20055a564e..e415ec64c38 100644
--- a/app/views/shared/_personal_access_tokens_form.html.haml
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -23,18 +23,3 @@
.prepend-top-default
= f.submit "Create #{type} token", class: "btn btn-create"
-
-:javascript
- var $dateField = $('.datepicker');
- var date = $dateField.val();
-
- new Pikaday({
- field: $dateField.get(0),
- theme: 'gitlab-theme animate-picker',
- format: 'yyyy-mm-dd',
- minDate: new Date(),
- container: $dateField.parent().get(0),
- onSelect: function(dateText) {
- $dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
- }
- });
diff --git a/app/views/shared/icons/_icon_status_notfound_borderless.svg b/app/views/shared/icons/_icon_status_notfound_borderless.svg
new file mode 100644
index 00000000000..e58bd264ef8
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_notfound_borderless.svg
@@ -0,0 +1 @@
+<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0V11.78a5.9 5.9 0 0 0 .827-.492z" fill-rule="nonzero"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 2cabbc8c560..c4ed7f6e750 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -36,13 +36,3 @@
.row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
- if has_labels
= render 'shared/labels_row', labels: @labels
-
-:javascript
- new LabelsSelect();
- new MilestoneSelect();
- new IssueStatusSelect();
- new SubscriptionSelect();
- $('form.filter-form').on('submit', function (event) {
- event.preventDefault();
- gl.utils.visitUrl(this.action + '&' + $(this).serialize());
- });
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index db407363a09..8a71819aa8e 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -16,5 +16,3 @@
.hide-collapsed.participants-more
%a.js-participants-more{ href: "#", data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } }
+ #{participants_extra} more
-:javascript
- IssuableContext.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row};
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 6f0b7600698..3428d6e0445 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -108,12 +108,3 @@
#js-add-issues-btn.prepend-left-10
- elsif type != :boards_modal
= render 'shared/sort_dropdown'
-
-- unless type === :boards_modal
- :javascript
- $(document).off('page:restore').on('page:restore', function (event) {
- if (gl.FilteredSearchManager) {
- const filteredSearchManager = new gl.FilteredSearchManager();
- filteredSearchManager.setup();
- }
- });
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index ecbaa901792..b08267357e5 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -138,17 +138,4 @@
= project_ref
= clipboard_button(text: project_ref, title: "Copy reference to clipboard", placement: "left")
- :javascript
- gl.sidebarOptions = {
- endpoint: "#{issuable_json_path(issuable)}?basic=true",
- editable: #{can_edit_issuable ? true : false},
- currentUser: #{current_user.to_json(only: [:username, :id, :name], methods: :avatar_url)},
- rootPath: "#{root_path}"
- };
-
- new MilestoneSelect('{"full_path":"#{@project.full_path}"}');
- new LabelsSelect();
- new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
- gl.Subscription.bindAll('.subscription');
- new gl.DueDateSelectors();
- window.sidebar = new Sidebar();
+ %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable, can_edit_issuable).to_json.html_safe
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index f0fcc414756..eae04c9bbb8 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -22,5 +22,4 @@
= link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes')
to comment
-:javascript
- var notes = new Notes("#{notes_url}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}", #{autocomplete})
+%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 4bdbc26a4c3..f4f155c8d94 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -14,7 +14,7 @@
- if avatar
.avatar-container.s40
= link_to project_path(project), class: dom_class(project) do
- - if use_creator_avatar
+ - if project.creator && use_creator_avatar
= image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
- else
= project_icon(project, alt: '', class: 'avatar project-avatar s40')
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index ac3701233ad..dfea8b40bd8 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,4 +1,3 @@
-- remote = local_assigns.fetch(:remote, false)
- link_project = local_assigns.fetch(:link_project, false)
.snippets-list-holder
@@ -8,7 +7,4 @@
%li
.nothing-here-block Nothing here.
- = paginate @snippets, theme: 'gitlab', remote: remote
-
-:javascript
- gl.SnippetsList();
+ = paginate @snippets, theme: 'gitlab'
diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml
deleted file mode 100644
index 57b8845c55d..00000000000
--- a/app/views/users/calendar.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-.clearfix.calendar
- .js-contrib-calendar
- .calendar-hint
- Summary of issues, merge requests, push events, and comments
-:javascript
- new Calendar(
- #{@activity_dates.to_json},
- '#{user_calendar_activities_path}'
- );
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 919ba5d15d3..a449706c567 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -2,9 +2,6 @@
- @hide_breadcrumbs = true
- page_title @user.name
- page_description @user.bio
-- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('common_d3')
- = page_specific_javascript_bundle_tag('users')
- header_title @user.name, user_path(@user)
- @no_container = true
@@ -107,7 +104,7 @@
.tab-content
#activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs
- .user-calendar{ data: { href: user_calendar_path } }
+ .user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path } }
%h4.center.light
%i.fa.fa-spinner.fa-spin
.user-calendar-activities
@@ -131,10 +128,3 @@
.loading-status
= spinner
-
-:javascript
- var userProfile;
-
- userProfile = new gl.User({
- action: "#{controller.action_name}"
- });
diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb
new file mode 100644
index 00000000000..4f47717ff69
--- /dev/null
+++ b/app/workers/create_gpg_signature_worker.rb
@@ -0,0 +1,16 @@
+class CreateGpgSignatureWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(commit_sha, project_id)
+ project = Project.find_by(id: project_id)
+
+ return unless project
+
+ commit = project.commit(commit_sha)
+
+ return unless commit
+
+ commit.signature
+ end
+end
diff --git a/app/workers/invalid_gpg_signature_update_worker.rb b/app/workers/invalid_gpg_signature_update_worker.rb
new file mode 100644
index 00000000000..db6b1ea8e8d
--- /dev/null
+++ b/app/workers/invalid_gpg_signature_update_worker.rb
@@ -0,0 +1,12 @@
+class InvalidGpgSignatureUpdateWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(gpg_key_id)
+ gpg_key = GpgKey.find_by(id: gpg_key_id)
+
+ return unless gpg_key
+
+ Gitlab::Gpg::InvalidGpgSignatureUpdater.new(gpg_key).run
+ end
+end