diff options
author | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2017-07-31 10:50:10 +0200 |
---|---|---|
committer | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2017-07-31 10:50:10 +0200 |
commit | 8f1274ae0350590a8a0d8f16558d24a9514b78c1 (patch) | |
tree | 514a6628a0a45dac10608b408cf3f011c893431b /app | |
parent | 79a7f7b6e59fa1225c440547796331caedabeaab (diff) | |
parent | 9a3b283402b8cc1c86802c526f19a459ce09c2e3 (diff) | |
download | gitlab-ce-8f1274ae0350590a8a0d8f16558d24a9514b78c1.tar.gz |
Merge commit '9a3b283402b8cc1c86802c526f19a459ce09c2e3' into backstage/gb/migrate-stages-statuses
* commit '9a3b283402b8cc1c86802c526f19a459ce09c2e3': (270 commits)
Add a note about EFS and GitLab log files
Projects logo are not centered vertically on projects page
Fix spec/features/projects/branches_spec
Fixup POST /v3/:id/hooks and PUT /v3/:id/hooks/:hook_id
Fix a spec that was assuming to be on the wrong page
Add copy about search terms to ux guide
Update documentation of user creation by replacing the 'confirm' param with 'skip_confirmation'
Fix replying to commit comments on MRs from forks
Fix 500 error when rendering avatar for deleted project creator
Load and process at most 100 commits when pushing into default branch
Ensure Gitlab::Application.routes.default_url_options are set correctly in Capybara + :js specs
Add log messages to clarify log messages about API CSRF token verification failure
Update gitlab_flow.md, Teatro seems to be completely dead, see also https://forum.gitlab.com/t/gitlab-flow-documentation-teatro/7774
Fix diff commenting results just after changing view
Update CHANGELOG.md for 9.4.2
none is not a CSS Value for sizes ;-)
Merge issuable "reopened" state into "opened"
Make access level more compatible with EE
Add link to JIRA article in docs
Expand pipeline_trigger_service_spec by godfat request
...
Diffstat (limited to 'app')
135 files changed, 1448 insertions, 654 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 ffe97c071ba..9d706b5ba59 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -20,6 +20,8 @@ /* global NamespaceSelects */ /* global Project */ /* global ProjectAvatar */ +/* global MergeRequest */ +/* global Compare */ /* global CompareAutocomplete */ /* global ProjectNew */ /* global ProjectShow */ @@ -62,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; @@ -156,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(); @@ -166,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(); @@ -221,6 +231,19 @@ import PerformanceBar from './performance_bar'; new gl.IssuableTemplateSelectors(); break; case 'projects:merge_requests:creations:new': + const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); + if (mrNewCompareNode) { + new Compare({ + targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl, + sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl, + targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl, + }); + } else { + const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit'); + new MergeRequest({ + action: mrNewSubmitNode.dataset.mrSubmitAction, + }); + } case 'projects:merge_requests:creations:diffs': case 'projects:merge_requests:edit': new gl.Diff(); @@ -237,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': @@ -257,15 +283,18 @@ 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, + }); break; 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(); @@ -274,6 +303,7 @@ import PerformanceBar from './performance_bar'; new MiniPipelineGraph({ container: '.js-commit-pipeline-graph', }).bindEvents(); + initNotes(); break; case 'projects:commit:pipelines': new MiniPipelineGraph({ @@ -281,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; @@ -367,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 @@ -415,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': @@ -515,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/how_to_merge.js b/app/assets/javascripts/how_to_merge.js index f739db751a6..19f4a946f73 100644 --- a/app/assets/javascripts/how_to_merge.js +++ b/app/assets/javascripts/how_to_merge.js @@ -3,10 +3,10 @@ document.addEventListener('DOMContentLoaded', () => { modal: true, show: false, }); - $('.how_to_merge_link').bind('click', () => { + $('.how_to_merge_link').on('click', () => { modal.show(); }); - $('.modal-header .close').bind('click', () => { + $('.modal-header .close').on('click', () => { modal.hide(); }); }); 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/notes.js b/app/assets/javascripts/notes.js index b2c503d1656..dfa07a2def4 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -529,6 +529,7 @@ export default class Notes { form.find('#note_line_code').remove(); form.find('#note_position').remove(); form.find('#note_type').val(''); + form.find('#note_project_id').remove(); form.find('#in_reply_to_discussion_id').remove(); form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove(); this.parentTimeline = form.parents('.timeline'); @@ -556,6 +557,7 @@ export default class Notes { form.find('#note_noteable_id').val(), form.find('#note_commit_id').val(), form.find('#note_type').val(), + form.find('#note_project_id').val(), form.find('#in_reply_to_discussion_id').val(), // LegacyDiffNote @@ -848,6 +850,8 @@ export default class Notes { form.find('#in_reply_to_discussion_id').val(discussionID); } + form.find('#note_project_id').val(dataHolder.data('discussionProjectId')); + form.attr('data-line-code', dataHolder.data('lineCode')); form.find('#line_type').val(dataHolder.data('lineType')); 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/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 0dfa7a31d31..cb41df8a88d 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -88,6 +88,10 @@ overflow: hidden; display: flex; + a { + display: flex; + } + .avatar { border-radius: 0; border: none; 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/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index aa04e490649..eb269df46fe 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -211,6 +211,10 @@ -webkit-overflow-scrolling: touch; } + &.affix-top .issuable-sidebar { + height: 100%; + } + &.right-sidebar-expanded { width: $gutter_width; 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/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index a57d9e6e6c0..af5f683bab5 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -4,6 +4,7 @@ module NotesActions included do before_action :authorize_admin_note!, only: [:update, :destroy] + before_action :note_project, only: [:create] end def index @@ -28,7 +29,8 @@ module NotesActions merge_request_diff_head_sha: params[:merge_request_diff_head_sha], in_reply_to_discussion_id: params[:in_reply_to_discussion_id] ) - @note = Notes::CreateService.new(project, current_user, create_params).execute + + @note = Notes::CreateService.new(note_project, current_user, create_params).execute if @note.is_a?(Note) Banzai::NoteRenderer.render([@note], @project, current_user) @@ -177,4 +179,22 @@ module NotesActions def notes_finder @notes_finder ||= NotesFinder.new(project, current_user, finder_params) end + + def note_project + return @note_project if defined?(@note_project) + return nil unless project + + note_project_id = params[:note_project_id] + + @note_project = + if note_project_id.present? + Project.find(note_project_id) + else + project + end + + return access_denied! unless can?(current_user, :create_note, @note_project) + + @note_project + end end 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 fc63e30c8fb..08a843ada97 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -11,6 +11,7 @@ # group_id: integer # project_id: integer # milestone_title: string +# author_id: integer # assignee_id: integer # search: string # label_name: string @@ -80,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 0a0881d95cf..e857e837c16 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -62,7 +62,11 @@ module NotesHelper def link_to_reply_discussion(discussion, line_type = nil) return unless current_user - data = { discussion_id: discussion.reply_id, line_type: line_type } + data = { + discussion_id: discussion.reply_id, + discussion_project_id: discussion.project&.id, + line_type: line_type + } button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', data: data, title: 'Add a reply' @@ -130,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/merge_request_diff.rb b/app/models/merge_request_diff.rb index 4b141945ab4..ec87aee9310 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -236,10 +236,21 @@ class MergeRequestDiff < ActiveRecord::Base def create_merge_request_diff_files(diffs) rows = diffs.map.with_index do |diff, index| - diff.to_hash.merge( + diff_hash = diff.to_hash.merge( + binary: false, merge_request_diff_id: self.id, relative_order: index ) + + # Compatibility with old diffs created with Psych. + diff_hash.tap do |hash| + diff_text = hash[:diff] + + if diff_text.encoding == Encoding::BINARY && !diff_text.ascii_only? + hash[:binary] = true + hash[:diff] = [diff_text].pack('m0') + end + end end Gitlab::Database.bulk_insert('merge_request_diff_files', rows) @@ -268,9 +279,7 @@ class MergeRequestDiff < ActiveRecord::Base st_diffs end elsif merge_request_diff_files.present? - merge_request_diff_files - .as_json(only: Gitlab::Git::Diff::SERIALIZE_KEYS) - .map(&:with_indifferent_access) + merge_request_diff_files.map(&:to_hash) end end diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb index 598ebd4d829..1199ff5af22 100644 --- a/app/models/merge_request_diff_file.rb +++ b/app/models/merge_request_diff_file.rb @@ -8,4 +8,14 @@ class MergeRequestDiffFile < ActiveRecord::Base encode_utf8(diff) if diff.respond_to?(:encoding) end + + def diff + binary? ? super.unpack('m0').first : super + end + + def to_hash + keys = Gitlab::Git::Diff::SERIALIZE_KEYS - [:diff] + + as_json(only: keys).merge(diff: diff).with_indifferent_access + end end 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..ada2b64a3a6 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -45,6 +45,7 @@ class GitPushService < BaseService elsif push_to_existing_branch? # Collect data for this git push @push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev]) + process_commit_messages # Update the bare repositories info/attributes file using the contents of the default branches @@ -56,6 +57,8 @@ class GitPushService < BaseService perform_housekeeping update_caches + + update_signatures end def update_gitattributes @@ -64,15 +67,21 @@ class GitPushService < BaseService def update_caches if is_default_branch? - paths = Set.new + if push_to_new_branch? + # If this is the initial push into the default branch, the file type caches + # will already be reset as a result of `Project#change_head`. + types = [] + else + paths = Set.new - @push_commits.each do |commit| - commit.raw_deltas.each do |diff| - paths << diff.new_path + @push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit| + commit.raw_deltas.each do |diff| + paths << diff.new_path + end end - end - types = Gitlab::FileDetector.types_in_paths(paths.to_a) + types = Gitlab::FileDetector.types_in_paths(paths.to_a) + end else types = [] end @@ -80,11 +89,17 @@ 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? - push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit| + @push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit| if commit.matches_cross_reference_regex? ProcessCommitWorker .perform_async(project.id, current_user.id, commit.to_hash, default) @@ -103,7 +118,7 @@ class GitPushService < BaseService EventCreateService.new.push(@project, current_user, build_push_data) Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute(:push) - + SystemHookPushWorker.perform_async(build_push_data.dup, :push_hooks) @project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks) @@ -123,7 +138,10 @@ class GitPushService < BaseService end def process_default_branch - @push_commits = project.repository.commits(params[:newrev]) + @push_commits_count = project.repository.commit_count_for_ref(params[:ref]) + + offset = [@push_commits_count - PROCESS_COMMIT_LIMIT, 0].max + @push_commits = project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT) # Ensure HEAD points to the default branch in case it is not master project.change_head(branch_name) @@ -152,7 +170,8 @@ class GitPushService < BaseService params[:oldrev], params[:newrev], params[:ref], - push_commits) + @push_commits, + commits_count: @push_commits_count) end def push_to_existing_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/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index e2b2660ea71..f6e8b6655f2 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -15,40 +15,48 @@ module Projects def execute return false unless can?(current_user, :remove_project, project) - repo_path = project.path_with_namespace - wiki_path = repo_path + '.wiki' - # Flush the cache for both repositories. This has to be done _before_ # removing the physical repositories as some expiration code depends on # Git data (e.g. a list of branch names). - flush_caches(project, wiki_path) + flush_caches(project) Projects::UnlinkForkService.new(project, current_user).execute - Project.transaction do - project.team.truncate - project.destroy! - - unless remove_legacy_registry_tags - raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.') - end - - unless remove_repository(repo_path) - raise_error('Failed to remove project repository. Please try again or contact administrator.') - end + attempt_destroy_transaction(project) - unless remove_repository(wiki_path) - raise_error('Failed to remove wiki repository. Please try again or contact administrator.') - end - end - - log_info("Project \"#{project.path_with_namespace}\" was removed") system_hook_service.execute_hooks_for(project, :destroy) + log_info("Project \"#{project.full_path}\" was removed") + true + rescue => error + attempt_rollback(project, error.message) + false + rescue Exception => error # rubocop:disable Lint/RescueException + # Project.transaction can raise Exception + attempt_rollback(project, error.message) + raise end private + def repo_path + project.path_with_namespace + end + + def wiki_path + repo_path + '.wiki' + end + + def trash_repositories! + unless remove_repository(repo_path) + raise_error('Failed to remove project repository. Please try again or contact administrator.') + end + + unless remove_repository(wiki_path) + raise_error('Failed to remove wiki repository. Please try again or contact administrator.') + end + end + def remove_repository(path) # Skip repository removal. We use this flag when remove user or group return true if params[:skip_repo] == true @@ -70,6 +78,26 @@ module Projects end end + def attempt_rollback(project, message) + return unless project + + project.update_attributes(delete_error: message, pending_delete: false) + log_error("Deletion failed on #{project.full_path} with the following message: #{message}") + end + + def attempt_destroy_transaction(project) + Project.transaction do + unless remove_legacy_registry_tags + raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.') + end + + trash_repositories! + + project.team.truncate + project.destroy! + end + end + ## # This method makes sure that we correctly remove registry tags # for legacy image repository (when repository path equals project path). @@ -96,7 +124,7 @@ module Projects "#{path}+#{project.id}#{DELETED_FLAG}" end - def flush_caches(project, wiki_path) + def flush_caches(project) project.repository.before_delete Repository.new(wiki_path, project).before_delete diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index e60b854f916..749a1cc56d8 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -130,7 +130,11 @@ module Projects end def max_size - current_application_settings.max_pages_size.megabytes || MAX_SIZE + max_pages_size = current_application_settings.max_pages_size.megabytes + + return MAX_SIZE if max_pages_size.zero? + + [max_pages_size, MAX_SIZE].min end def tmp_path 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/_deletion_failed.html.haml b/app/views/projects/_deletion_failed.html.haml new file mode 100644 index 00000000000..4f3698f91e6 --- /dev/null +++ b/app/views/projects/_deletion_failed.html.haml @@ -0,0 +1,6 @@ +- project = local_assigns.fetch(:project) +- return unless project.delete_error.present? + +.project-deletion-failed-message.alert.alert-warning + This project was scheduled for deletion, but failed with the following message: + = project.delete_error diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml new file mode 100644 index 00000000000..f47d84ef755 --- /dev/null +++ b/app/views/projects/_flash_messages.html.haml @@ -0,0 +1,8 @@ +- project = local_assigns.fetch(:project) +- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message + += content_for flash_message_container do + = render partial: 'deletion_failed', locals: { project: project } + - if current_user && can?(current_user, :download_code, project) + = render 'shared/no_ssh' + = render 'shared/no_password' 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/empty.html.haml b/app/views/projects/empty.html.haml index 0f132a68ce1..d17709380d5 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,10 +1,6 @@ - @no_container = true -- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message -= content_for flash_message_container do - - if current_user && can?(current_user, :download_code, @project) - = render 'shared/no_ssh' - = render 'shared/no_password' += render partial: 'flash_messages', locals: { project: @project } = render "projects/head" = render "home_panel" diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index d02ea5cccc3..4b9da02c6b8 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,6 +1,7 @@ - @no_container = true - page_title "Labels" - hide_class = '' +- can_admin_label = can?(current_user, :admin_label, @project) - if show_new_nav? && can?(current_user, :admin_label, @project) - content_for :breadcrumbs_extra do @@ -12,15 +13,17 @@ %div{ class: container_class } .top-area.adjust .nav-text - Labels can be applied to issues and merge requests. Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. + Labels can be applied to issues and merge requests. + - if can_admin_label + Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. - .nav-controls{ class: ("visible-xs" if show_new_nav?) } - - if can?(current_user, :admin_label, @project) + - if can_admin_label + .nav-controls{ class: ("visible-xs" if show_new_nav?) } = link_to new_project_label_path(@project), class: "btn btn-new" do New label .labels - - if can?(current_user, :admin_label, @project) + - if can_admin_label -# Only show it in the first page - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') .prioritized-labels{ class: ('hide' if hide) } @@ -33,7 +36,7 @@ - if @labels.present? .other-labels - - if can?(current_user, :admin_label, @project) + - if can_admin_label %h5{ class: ('hide' if hide) } Other Labels %ul.content-list.manage-labels-list.js-other-labels = render partial: 'shared/label', subject: @project, collection: @labels, as: :label diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml index 4e5aae496b1..8958b2cf5e1 100644 --- a/app/views/projects/merge_requests/creations/_new_compare.html.haml +++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml @@ -3,7 +3,7 @@ = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f| .hide.alert.alert-danger.mr-compare-errors - .merge-request-branches.row + .merge-request-branches.js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) } .col-md-6 .panel.panel-default.panel-new-merge-request .panel-heading @@ -66,10 +66,3 @@ - if @merge_request.errors.any? = form_errors(@merge_request) = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn" - -:javascript - new Compare({ - targetProjectUrl: "#{project_new_merge_request_update_branches_path(@source_project)}", - sourceBranchUrl: "#{project_new_merge_request_branch_from_path(@source_project)}", - targetBranchUrl: "#{project_new_merge_request_branch_to_path(@source_project)}" - }); diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index c72dd1d8e29..4b5fa28078a 100644 --- a/app/views/projects/merge_requests/creations/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -17,7 +17,7 @@ = f.hidden_field :target_project_id = f.hidden_field :target_branch -.mr-compare.merge-request +.mr-compare.merge-request.js-merge-request-new-submit{ 'data-mr-submit-action': "#{j params[:tab].presence || 'new'}" } - if @commits.empty? .commits-empty %h4 @@ -50,8 +50,3 @@ .mr-loading-status = spinner - -:javascript - var merge_request = new MergeRequest({ - action: "#{j params[:tab].presence || 'new'}", - }); diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 2efc1d68190..ea6cd16c7ad 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -3,10 +3,10 @@ - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('diff_notes') + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag('diff_notes') -.merge-request{ 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) } +.merge-request{ 'data-mr-action': "#{j params[:tab].presence || 'show'}", 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) } = render "projects/merge_requests/mr_title" .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } @@ -15,13 +15,13 @@ - if @merge_request.source_branch_exists? = render "projects/merge_requests/how_to_merge" + -# haml-lint:disable InlineJavaScript :javascript window.gl.mrWidgetData = #{serialize_issuable(@merge_request)} #js-vue-mr-widget.mr-widget - content_for :page_specific_javascripts do - = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'vue_merge_request_widget' .content-block.content-block-small.emoji-list-container @@ -88,10 +88,3 @@ = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title - if @merge_request.can_be_cherry_picked? = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title - -:javascript - $(function () { - window.mergeRequest = new MergeRequest({ - action: "#{j params[:tab].presence || 'show'}", - }); - }); diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index a2d7a21d5f6..87cc23fc649 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -72,7 +72,7 @@ %div - if fogbugz_import_enabled? = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do - = icon('bug', text: 'Fogbugz') + = icon('bug', text: 'FogBugz') %div - if gitea_import_enabled? = link_to new_import_gitea_url, class: 'btn import_gitea' do diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 49d0a6828fe..a9b39cedb1d 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,15 +1,11 @@ - @no_container = true - breadcrumb_title "Project" - @content_class = "limit-container-width" unless fluid_layout -- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message = content_for :meta_tags do = auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity") -= content_for flash_message_container do - - if current_user && can?(current_user, :download_code, @project) - = render 'shared/no_ssh' - = render 'shared/no_password' += render partial: 'flash_messages', locals: { project: @project } = render "projects/head" = render "projects/last_push" 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/milestones/_participants_tab.html.haml b/app/views/shared/milestones/_participants_tab.html.haml index 549d2e2f61e..1615871385e 100644 --- a/app/views/shared/milestones/_participants_tab.html.haml +++ b/app/views/shared/milestones/_participants_tab.html.haml @@ -4,5 +4,5 @@ = link_to user, title: user.name, class: "darken" do = image_tag avatar_icon(user, 32), class: "avatar s32" %strong= truncate(user.name, length: 40) - %br - %small.cgray= user.username + %div + %small.cgray= user.username diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml index c6b5dcc3647..725bf916592 100644 --- a/app/views/shared/notes/_form.html.haml +++ b/app/views/shared/notes/_form.html.haml @@ -10,6 +10,7 @@ = hidden_field_tag :line_type = hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha) = hidden_field_tag :in_reply_to_discussion_id + = hidden_field_tag :note_project_id = note_target_fields(@note) = f.hidden_field :noteable_type 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 diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb index b462327490e..a9188b78460 100644 --- a/app/workers/project_destroy_worker.rb +++ b/app/workers/project_destroy_worker.rb @@ -3,14 +3,11 @@ class ProjectDestroyWorker include DedicatedSidekiqQueue def perform(project_id, user_id, params) - begin - project = Project.unscoped.find(project_id) - rescue ActiveRecord::RecordNotFound - return - end - + project = Project.find(project_id) user = User.find(user_id) ::Projects::DestroyService.new(project, user, params.symbolize_keys).execute + rescue ActiveRecord::RecordNotFound => error + logger.error("Failed to delete project (#{project_id}): #{error.message}") end end |