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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorPawel Chojnacki <pawel@chojnacki.ws>2017-05-30 11:12:45 +0300
committerPawel Chojnacki <pawel@chojnacki.ws>2017-05-30 11:12:45 +0300
commitc24d20da49fb77332f32b1b6aebda7615a7e8674 (patch)
tree853ddb94ff28c03a596b683c15015b7d574cacb6 /app
parent608186d54bb9aefb0b867c177ac62d534e8840ad (diff)
parent38d953864af7e88eb0ee74324d9deaa0a0210993 (diff)
Merge remote-tracking branch 'upstream/master' into 28717-additional-metrics-review-branch
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/dispatcher.js2
-rw-r--r--app/assets/javascripts/gl_dropdown.js10
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js10
-rw-r--r--app/assets/javascripts/merge_request_tabs.js2
-rw-r--r--app/assets/javascripts/notes.js225
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue68
-rw-r--r--app/assets/javascripts/pipelines/graph_bundle.js10
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js33
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediatior.js51
-rw-r--r--app/assets/javascripts/pipelines/stores/pipeline_store.js6
-rw-r--r--app/assets/javascripts/single_file_diff.js2
-rw-r--r--app/assets/javascripts/users_select.js34
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js44
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue122
-rw-r--r--app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue58
-rw-r--r--app/assets/javascripts/vue_shared/mixins/timeago.js18
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/awards.scss2
-rw-r--r--app/assets/stylesheets/framework/blocks.scss1
-rw-r--r--app/assets/stylesheets/framework/emojis.scss1
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/flash.scss4
-rw-r--r--app/assets/stylesheets/framework/lists.scss1
-rw-r--r--app/assets/stylesheets/framework/notes.scss14
-rw-r--r--app/assets/stylesheets/framework/selects.scss1
-rw-r--r--app/assets/stylesheets/framework/timeline.scss34
-rw-r--r--app/assets/stylesheets/framework/typography.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/boards.scss2
-rw-r--r--app/assets/stylesheets/pages/ci_projects.scss1
-rw-r--r--app/assets/stylesheets/pages/diff.scss7
-rw-r--r--app/assets/stylesheets/pages/environments.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/issues.scss1
-rw-r--r--app/assets/stylesheets/pages/members.scss2
-rw-r--r--app/assets/stylesheets/pages/note_form.scss12
-rw-r--r--app/assets/stylesheets/pages/notes.scss105
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss5
-rw-r--r--app/controllers/admin/hook_logs_controller.rb29
-rw-r--r--app/controllers/admin/hooks_controller.rb32
-rw-r--r--app/controllers/application_controller.rb8
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/concerns/diff_for_path.rb13
-rw-r--r--app/controllers/concerns/hooks_execution.rb15
-rw-r--r--app/controllers/dashboard/projects_controller.rb3
-rw-r--r--app/controllers/dashboard_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb3
-rw-r--r--app/controllers/projects/compare_controller.rb6
-rw-r--r--app/controllers/projects/hook_logs_controller.rb33
-rw-r--r--app/controllers/projects/hooks_controller.rb17
-rw-r--r--app/controllers/projects/merge_requests_controller.rb9
-rw-r--r--app/controllers/projects/refs_controller.rb2
-rw-r--r--app/helpers/commits_helper.rb10
-rw-r--r--app/helpers/diff_helper.rb18
-rw-r--r--app/helpers/labels_helper.rb5
-rw-r--r--app/helpers/projects_helper.rb34
-rw-r--r--app/helpers/submodule_helper.rb1
-rw-r--r--app/mailers/base_mailer.rb6
-rw-r--r--app/models/ci/pipeline_schedule.rb10
-rw-r--r--app/models/concerns/note_on_diff.rb10
-rw-r--r--app/models/concerns/routable.rb83
-rw-r--r--app/models/concerns/select_for_project_authorization.rb6
-rw-r--r--app/models/diff_note.rb4
-rw-r--r--app/models/group.rb6
-rw-r--r--app/models/hooks/service_hook.rb2
-rw-r--r--app/models/hooks/system_hook.rb4
-rw-r--r--app/models/hooks/web_hook.rb43
-rw-r--r--app/models/hooks/web_hook_log.rb13
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/legacy_diff_note.rb2
-rw-r--r--app/models/merge_request.rb34
-rw-r--r--app/models/merge_request_diff.rb23
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/namespace.rb24
-rw-r--r--app/models/project.rb10
-rw-r--r--app/models/project_authorization.rb6
-rw-r--r--app/models/project_services/jira_service.rb37
-rw-r--r--app/models/project_services/kubernetes_service.rb24
-rw-r--r--app/models/user.rb40
-rw-r--r--app/serializers/merge_request_entity.rb1
-rw-r--r--app/services/issues/close_service.rb1
-rw-r--r--app/services/issues/reopen_service.rb1
-rw-r--r--app/services/merge_requests/close_service.rb1
-rw-r--r--app/services/merge_requests/post_merge_service.rb1
-rw-r--r--app/services/merge_requests/reopen_service.rb1
-rw-r--r--app/services/search_service.rb2
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb40
-rw-r--r--app/services/web_hook_service.rb120
-rw-r--r--app/validators/dynamic_path_validator.rb22
-rw-r--r--app/views/admin/hook_logs/_index.html.haml37
-rw-r--r--app/views/admin/hook_logs/show.html.haml10
-rw-r--r--app/views/admin/hooks/edit.html.haml6
-rw-r--r--app/views/admin/system_info/show.html.haml5
-rw-r--r--app/views/dashboard/_activities.html.haml3
-rw-r--r--app/views/dashboard/activity.html.haml12
-rw-r--r--app/views/dashboard/projects/index.html.haml22
-rw-r--r--app/views/dashboard/projects/starred.html.haml18
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml2
-rw-r--r--app/views/discussions/_notes.html.haml14
-rw-r--r--app/views/events/_event_last_push.html.haml14
-rw-r--r--app/views/events/event/_push.html.haml2
-rw-r--r--app/views/groups/_activities.html.haml3
-rw-r--r--app/views/groups/_head.html.haml3
-rw-r--r--app/views/groups/_show_nav.html.haml7
-rw-r--r--app/views/groups/show.html.haml1
-rw-r--r--app/views/layouts/nav/_admin.html.haml4
-rw-r--r--app/views/notify/repository_push_email.html.haml28
-rw-r--r--app/views/notify/repository_push_email.text.haml20
-rw-r--r--app/views/projects/_activity.html.haml2
-rw-r--r--app/views/projects/_last_push.html.haml34
-rw-r--r--app/views/projects/activity.html.haml2
-rw-r--r--app/views/projects/blob/show.html.haml5
-rw-r--r--app/views/projects/boards/components/sidebar/_assignee.html.haml5
-rw-r--r--app/views/projects/diffs/_content.html.haml21
-rw-r--r--app/views/projects/diffs/_diffs.html.haml10
-rw-r--r--app/views/projects/diffs/_file.html.haml12
-rw-r--r--app/views/projects/diffs/_file_header.html.haml16
-rw-r--r--app/views/projects/diffs/_image.html.haml69
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml2
-rw-r--r--app/views/projects/diffs/_stats.html.haml6
-rw-r--r--app/views/projects/diffs/_text_file.html.haml4
-rw-r--r--app/views/projects/diffs/viewers/_image.html.haml68
-rw-r--r--app/views/projects/diffs/viewers/_text.html.haml8
-rw-r--r--app/views/projects/hook_logs/_index.html.haml37
-rw-r--r--app/views/projects/hook_logs/show.html.haml11
-rw-r--r--app/views/projects/hooks/edit.html.haml8
-rw-r--r--app/views/projects/merge_requests/index.html.haml4
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml3
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml6
-rw-r--r--app/views/projects/pipelines/charts/_overall.haml6
-rw-r--r--app/views/projects/pipelines/show.html.haml6
-rw-r--r--app/views/projects/settings/_head.html.haml2
-rw-r--r--app/views/projects/tree/show.html.haml1
-rw-r--r--app/views/search/_category.html.haml77
-rw-r--r--app/views/shared/_group_form.html.haml2
-rw-r--r--app/views/shared/hook_logs/_content.html.haml44
-rw-r--r--app/views/shared/hook_logs/_status_label.html.haml3
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/form/_merge_params.html.haml10
-rw-r--r--app/views/shared/issuable/form/_metadata_issue_assignee.html.haml2
-rw-r--r--app/views/shared/notes/_note.html.haml2
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml23
-rw-r--r--app/workers/process_commit_worker.rb15
-rw-r--r--app/workers/remove_old_web_hook_logs_worker.rb10
-rw-r--r--app/workers/system_hook_worker.rb10
-rw-r--r--app/workers/web_hook_worker.rb (renamed from app/workers/project_web_hook_worker.rb)6
151 files changed, 1561 insertions, 953 deletions
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 2090a7e12d6..c5fffea8bb0 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -123,7 +123,7 @@ import ShortcutsBlob from './shortcuts_blob';
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
- if (gl.FilteredSearchManager) {
+ if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) {
new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
}
Issuable.init();
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 24c423dd01e..d34561e5512 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -468,8 +468,8 @@ GitLabDropdown = (function() {
// Process the data to make sure rendered data
// matches the correct layout
- if (this.fullData && hasMultiSelect && this.options.processData) {
- const inputValue = this.filterInput.val();
+ const inputValue = this.filterInput.val();
+ if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) {
this.options.processData.call(this.options, inputValue, this.filteredFullData(), this.parseData.bind(this));
}
@@ -740,6 +740,12 @@ GitLabDropdown = (function() {
$input.attr('id', this.options.inputId);
}
+ if (this.options.multiSelect) {
+ Object.keys(selectedObject).forEach((attribute) => {
+ $input.attr(`data-${attribute}`, selectedObject[attribute]);
+ });
+ }
+
if (this.options.inputMeta) {
$input.attr('data-meta', selectedObject[this.options.inputMeta]);
}
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index f1b07408671..57394097944 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -42,3 +42,13 @@ export function formatRelevantDigits(number) {
export function bytesToKiB(number) {
return number / BYTES_IN_KIB;
}
+
+/**
+ * Utility function that calculates MiB of the given bytes.
+ *
+ * @param {Number} number bytes
+ * @return {Number} MiB
+ */
+export function bytesToMiB(number) {
+ return number / (BYTES_IN_KIB * BYTES_IN_KIB);
+}
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 22032d0f914..894ed81b044 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -285,7 +285,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
// Similar to `toggler_behavior` in the discussion tab
const hash = window.gl.utils.getLocationHash();
const anchor = hash && $container.find(`[id="${hash}"]`);
- if (anchor) {
+ if (anchor && anchor.length > 0) {
const notesContent = anchor.closest('.notes_content');
const lineType = notesContent.hasClass('new') ? 'new' : 'old';
notes.toggleDiffNote({
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index b0b1cfd6c8a..0ca7cabfc5a 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,4 +1,10 @@
-/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */
+/* eslint-disable no-restricted-properties, func-names, space-before-function-paren,
+no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase,
+no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line,
+default-case, prefer-template, consistent-return, no-alert, no-return-assign,
+no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new,
+brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow,
+newline-per-chained-call, no-useless-escape */
/* global Flash */
/* global Autosave */
/* global ResolveService */
@@ -57,7 +63,7 @@ const normalizeNewlines = function(str) {
this.updatedNotesTrackingMap = {};
this.last_fetched_at = last_fetched_at;
this.noteable_url = document.URL;
- this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge"));
+ this.notesCountBadge || (this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge'));
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
this.flashErrors = [];
@@ -87,61 +93,61 @@ const normalizeNewlines = function(str) {
Notes.prototype.addBinding = function() {
// Edit note link
- $(document).on("click", ".js-note-edit", this.showEditForm.bind(this));
- $(document).on("click", ".note-edit-cancel", this.cancelEdit);
+ $(document).on('click', '.js-note-edit', this.showEditForm.bind(this));
+ $(document).on('click', '.note-edit-cancel', this.cancelEdit);
// Reopen and close actions for Issue/MR combined with note form submit
- $(document).on("click", ".js-comment-submit-button", this.postComment);
- $(document).on("click", ".js-comment-save-button", this.updateComment);
- $(document).on("keyup input", ".js-note-text", this.updateTargetButtons);
+ $(document).on('click', '.js-comment-submit-button', this.postComment);
+ $(document).on('click', '.js-comment-save-button', this.updateComment);
+ $(document).on('keyup input', '.js-note-text', this.updateTargetButtons);
// resolve a discussion
$(document).on('click', '.js-comment-resolve-button', this.postComment);
// remove a note (in general)
- $(document).on("click", ".js-note-delete", this.removeNote);
+ $(document).on('click', '.js-note-delete', this.removeNote);
// delete note attachment
- $(document).on("click", ".js-note-attachment-delete", this.removeAttachment);
+ $(document).on('click', '.js-note-attachment-delete', this.removeAttachment);
// reset main target form when clicking discard
- $(document).on("click", ".js-note-discard", this.resetMainTargetForm);
+ $(document).on('click', '.js-note-discard', this.resetMainTargetForm);
// update the file name when an attachment is selected
- $(document).on("change", ".js-note-attachment-input", this.updateFormAttachment);
+ $(document).on('change', '.js-note-attachment-input', this.updateFormAttachment);
// reply to diff/discussion notes
- $(document).on("click", ".js-discussion-reply-button", this.onReplyToDiscussionNote);
+ $(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note
- $(document).on("click", ".js-add-diff-note-button", this.onAddDiffNote);
+ $(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// hide diff note form
- $(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm);
+ $(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
// toggle commit list
- $(document).on("click", '.system-note-commit-list-toggler', this.toggleCommitList);
+ $(document).on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
// fetch notes when tab becomes visible
- $(document).on("visibilitychange", this.visibilityChange);
+ $(document).on('visibilitychange', this.visibilityChange);
// when issue status changes, we need to refresh data
- $(document).on("issuable:change", this.refresh);
+ $(document).on('issuable:change', this.refresh);
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
- $(document).on("ajax:success", ".js-main-target-form", this.addNote);
- $(document).on("ajax:success", ".js-discussion-note-form", this.addDiscussionNote);
- $(document).on("ajax:success", ".js-main-target-form", this.resetMainTargetForm);
- $(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton);
+ $(document).on('ajax:success', '.js-main-target-form', this.addNote);
+ $(document).on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote);
+ $(document).on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
+ $(document).on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton);
// when a key is clicked on the notes
- return $(document).on("keydown", ".js-note-text", this.keydownNoteText);
+ return $(document).on('keydown', '.js-note-text', this.keydownNoteText);
};
Notes.prototype.cleanBinding = function() {
- $(document).off("click", ".js-note-edit");
- $(document).off("click", ".note-edit-cancel");
- $(document).off("click", ".js-note-delete");
- $(document).off("click", ".js-note-attachment-delete");
- $(document).off("click", ".js-discussion-reply-button");
- $(document).off("click", ".js-add-diff-note-button");
- $(document).off("visibilitychange");
- $(document).off("keyup input", ".js-note-text");
- $(document).off("click", ".js-note-target-reopen");
- $(document).off("click", ".js-note-target-close");
- $(document).off("click", ".js-note-discard");
- $(document).off("keydown", ".js-note-text");
+ $(document).off('click', '.js-note-edit');
+ $(document).off('click', '.note-edit-cancel');
+ $(document).off('click', '.js-note-delete');
+ $(document).off('click', '.js-note-attachment-delete');
+ $(document).off('click', '.js-discussion-reply-button');
+ $(document).off('click', '.js-add-diff-note-button');
+ $(document).off('visibilitychange');
+ $(document).off('keyup input', '.js-note-text');
+ $(document).off('click', '.js-note-target-reopen');
+ $(document).off('click', '.js-note-target-close');
+ $(document).off('click', '.js-note-discard');
+ $(document).off('keydown', '.js-note-text');
$(document).off('click', '.js-comment-resolve-button');
- $(document).off("click", '.system-note-commit-list-toggler');
- $(document).off("ajax:success", ".js-main-target-form");
- $(document).off("ajax:success", ".js-discussion-note-form");
- $(document).off("ajax:complete", ".js-main-target-form");
+ $(document).off('click', '.system-note-commit-list-toggler');
+ $(document).off('ajax:success', '.js-main-target-form');
+ $(document).off('ajax:success', '.js-discussion-note-form');
+ $(document).off('ajax:complete', '.js-main-target-form');
};
Notes.initCommentTypeToggle = function (form) {
@@ -231,8 +237,8 @@ const normalizeNewlines = function(str) {
this.refreshing = true;
return $.ajax({
url: this.notes_url,
- headers: { "X-Last-Fetched-At": this.last_fetched_at },
- dataType: "json",
+ headers: { 'X-Last-Fetched-At': this.last_fetched_at },
+ dataType: 'json',
success: (function(_this) {
return function(data) {
var notes;
@@ -303,7 +309,7 @@ const normalizeNewlines = function(str) {
*/
Notes.prototype.renderNote = function(noteEntity, $form, $notesList = $('.main-notes-list')) {
- if (noteEntity.discussion_html != null) {
+ if (noteEntity.discussion_html) {
return this.renderDiscussionNote(noteEntity, $form);
}
@@ -368,8 +374,8 @@ const normalizeNewlines = function(str) {
return;
}
this.note_ids.push(noteEntity.id);
- form = $form || $(".js-discussion-note-form[data-discussion-id='" + noteEntity.discussion_id + "']");
- row = form.closest("tr");
+ form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
+ row = form.closest('tr');
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion?
@@ -386,7 +392,7 @@ const normalizeNewlines = function(str) {
row.after($discussion);
} else {
// Merge new discussion HTML in
- var $notes = $discussion.find('.notes[data-discussion-id="' + noteEntity.discussion_id + '"]');
+ var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
var contentContainerClass = '.' + $notes.closest('.notes_content')
.attr('class')
.split(' ')
@@ -397,7 +403,7 @@ const normalizeNewlines = function(str) {
}
// Init discussion on 'Discussion' page if it is merge request page
const page = $('body').attr('data-page');
- if ((page && page.indexOf('projects:merge_request') === 0) || !noteEntity.diff_discussion_html) {
+ if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) {
Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
}
} else {
@@ -450,13 +456,13 @@ const normalizeNewlines = function(str) {
Notes.prototype.resetMainTargetForm = function(e) {
var form;
- form = $(".js-main-target-form");
+ form = $('.js-main-target-form');
// remove validation errors
- form.find(".js-errors").remove();
+ form.find('.js-errors').remove();
// reset text and preview
- form.find(".js-md-write-button").click();
- form.find(".js-note-text").val("").trigger("input");
- form.find(".js-note-text").data("autosave").reset();
+ form.find('.js-md-write-button').click();
+ form.find('.js-note-text').val('').trigger('input');
+ form.find('.js-note-text').data('autosave').reset();
var event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
@@ -467,8 +473,8 @@ const normalizeNewlines = function(str) {
Notes.prototype.reenableTargetFormSubmitButton = function() {
var form;
- form = $(".js-main-target-form");
- return form.find(".js-note-text").trigger("input");
+ form = $('.js-main-target-form');
+ return form.find('.js-note-text').trigger('input');
};
/*
@@ -480,18 +486,18 @@ const normalizeNewlines = function(str) {
Notes.prototype.setupMainTargetNoteForm = function() {
var form;
// find the form
- form = $(".js-new-note-form");
+ form = $('.js-new-note-form');
// Set a global clone of the form for later cloning
this.formClone = form.clone();
// show the form
this.setupNoteForm(form);
// fix classes
- form.removeClass("js-new-note-form");
- form.addClass("js-main-target-form");
- form.find("#note_line_code").remove();
- form.find("#note_position").remove();
- form.find("#note_type").val('');
- form.find("#in_reply_to_discussion_id").remove();
+ form.removeClass('js-new-note-form');
+ form.addClass('js-main-target-form');
+ form.find('#note_line_code').remove();
+ form.find('#note_position').remove();
+ form.find('#note_type').val('');
+ 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');
@@ -512,20 +518,20 @@ const normalizeNewlines = function(str) {
Notes.prototype.setupNoteForm = function(form) {
var textarea, key;
new gl.GLForm(form, this.enableGFM);
- textarea = form.find(".js-note-text");
+ textarea = form.find('.js-note-text');
key = [
- "Note",
- form.find("#note_noteable_type").val(),
- form.find("#note_noteable_id").val(),
- form.find("#note_commit_id").val(),
- form.find("#note_type").val(),
- form.find("#in_reply_to_discussion_id").val(),
+ 'Note',
+ form.find('#note_noteable_type').val(),
+ form.find('#note_noteable_id').val(),
+ form.find('#note_commit_id').val(),
+ form.find('#note_type').val(),
+ form.find('#in_reply_to_discussion_id').val(),
// LegacyDiffNote
- form.find("#note_line_code").val(),
+ form.find('#note_line_code').val(),
// DiffNote
- form.find("#note_position").val()
+ form.find('#note_position').val()
];
return new Autosave(textarea, key);
};
@@ -670,7 +676,8 @@ const normalizeNewlines = function(str) {
const $newNote = $(this.updatedNotesTrackingMap[noteId].html);
$note.replaceWith($newNote);
this.setupNewNote($newNote);
- this.updatedNotesTrackingMap[noteId] = null;
+ // Now that we have taken care of the update, clear it out
+ delete this.updatedNotesTrackingMap[noteId];
}
else {
$note.find('.js-finish-edit-warning').hide();
@@ -722,14 +729,14 @@ const normalizeNewlines = function(str) {
lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]')
.closest('.notes_holder')
.prev('.line_holder');
- $(".note[id='" + noteElId + "']").each((function(_this) {
+ $(`.note[id="${noteElId}"]`).each((function(_this) {
// A same note appears in the "Discussion" and in the "Changes" tab, we have
- // to remove all. Using $(".note[id='noteId']") ensure we get all the notes,
- // where $("#noteId") would return only one.
+ // to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
+ // where $('#noteId') would return only one.
return function(i, el) {
var $note, $notes;
$note = $(el);
- $notes = $note.closest(".discussion-notes");
+ $notes = $note.closest('.discussion-notes');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
if (gl.diffNoteApps[noteElId]) {
@@ -740,11 +747,11 @@ const normalizeNewlines = function(str) {
$note.remove();
// check if this is the last note for this line
- if ($notes.find(".note").length === 0) {
- var notesTr = $notes.closest("tr");
+ if ($notes.find('.note').length === 0) {
+ var notesTr = $notes.closest('tr');
// "Discussions" tab
- $notes.closest(".timeline-entry").remove();
+ $notes.closest('.timeline-entry').remove();
// The notes tr can contain multiple lists of notes, like on the parallel diff
if (notesTr.find('.discussion-notes').length > 1) {
@@ -768,11 +775,11 @@ const normalizeNewlines = function(str) {
*/
Notes.prototype.removeAttachment = function() {
- const $note = $(this).closest(".note");
- $note.find(".note-attachment").remove();
- $note.find(".note-body > .note-text").show();
- $note.find(".note-header").show();
- return $note.find(".current-note-edit-form").remove();
+ const $note = $(this).closest('.note');
+ $note.find('.note-attachment').remove();
+ $note.find('.note-body > .note-text').show();
+ $note.find('.note-header').show();
+ return $note.find('.current-note-edit-form').remove();
};
/*
@@ -788,7 +795,7 @@ const normalizeNewlines = function(str) {
Notes.prototype.replyToDiscussionNote = function(target) {
var form, replyLink;
form = this.cleanForm(this.formClone.clone());
- replyLink = $(target).closest(".js-discussion-reply-button");
+ replyLink = $(target).closest('.js-discussion-reply-button');
// insert the form after the button
replyLink
.closest('.discussion-reply-holder')
@@ -808,26 +815,26 @@ const normalizeNewlines = function(str) {
Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) {
// setup note target
- var discussionID = dataHolder.data("discussionId");
+ var discussionID = dataHolder.data('discussionId');
if (discussionID) {
- form.attr("data-discussion-id", discussionID);
- form.find("#in_reply_to_discussion_id").val(discussionID);
+ form.attr('data-discussion-id', discussionID);
+ form.find('#in_reply_to_discussion_id').val(discussionID);
}
- form.attr("data-line-code", dataHolder.data("lineCode"));
- form.find("#line_type").val(dataHolder.data("lineType"));
+ form.attr('data-line-code', dataHolder.data('lineCode'));
+ form.find('#line_type').val(dataHolder.data('lineType'));
- form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
- form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
- form.find("#note_commit_id").val(dataHolder.data("commitId"));
- form.find("#note_type").val(dataHolder.data("noteType"));
+ form.find('#note_noteable_type').val(dataHolder.data('noteableType'));
+ form.find('#note_noteable_id').val(dataHolder.data('noteableId'));
+ form.find('#note_commit_id').val(dataHolder.data('commitId'));
+ form.find('#note_type').val(dataHolder.data('noteType'));
// LegacyDiffNote
- form.find("#note_line_code").val(dataHolder.data("lineCode"));
+ form.find('#note_line_code').val(dataHolder.data('lineCode'));
// DiffNote
- form.find("#note_position").val(dataHolder.attr("data-position"));
+ form.find('#note_position').val(dataHolder.attr('data-position'));
form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text'));
form.find('.js-note-target-close').remove();
@@ -836,7 +843,7 @@ const normalizeNewlines = function(str) {
form
.removeClass('js-main-target-form')
- .addClass("discussion-form js-discussion-note-form");
+ .addClass('discussion-form js-discussion-note-form');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
var $commentBtn = form.find('comment-and-resolve-btn');
@@ -845,7 +852,7 @@ const normalizeNewlines = function(str) {
gl.diffNotesCompileComponents();
}
- form.find(".js-note-text").focus();
+ form.find('.js-note-text').focus();
form
.find('.js-comment-resolve-button')
.attr('data-discussion-id', discussionID);
@@ -878,21 +885,21 @@ const normalizeNewlines = function(str) {
}) {
var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
$link = $(target);
- row = $link.closest("tr");
+ row = $link.closest('tr');
const nextRow = row.next();
let targetRow = row;
if (nextRow.is('.notes_holder')) {
targetRow = nextRow;
}
- hasNotes = targetRow.is(".notes_holder");
+ hasNotes = nextRow.is('.notes_holder');
addForm = false;
let lineTypeSelector = '';
- rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>";
+ rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>';
// In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineTypeSelector = `.${lineType}`;
- rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>";
+ rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>';
}
const notesContentSelector = `.notes_content${lineTypeSelector} .content`;
let notesContent = targetRow.find(notesContentSelector);
@@ -902,12 +909,12 @@ const normalizeNewlines = function(str) {
notesContent = targetRow.find(notesContentSelector);
if (notesContent.length) {
notesContent.show();
- replyButton = notesContent.find(".js-discussion-reply-button:visible");
+ replyButton = notesContent.find('.js-discussion-reply-button:visible');
if (replyButton.length) {
this.replyToDiscussionNote(replyButton[0]);
} else {
// In parallel view, the form may not be present in one of the panes
- noteForm = notesContent.find(".js-discussion-note-form");
+ noteForm = notesContent.find('.js-discussion-note-form');
if (noteForm.length === 0) {
addForm = true;
}
@@ -945,15 +952,15 @@ const normalizeNewlines = function(str) {
Notes.prototype.removeDiscussionNoteForm = function(form) {
var glForm, row;
- row = form.closest("tr");
+ row = form.closest('tr');
glForm = form.data('gl-form');
glForm.destroy();
- form.find(".js-note-text").data("autosave").reset();
+ form.find('.js-note-text').data('autosave').reset();
// show the reply button (will only work for replies)
form
.prev('.discussion-reply-holder')
.show();
- if (row.is(".js-temp-notes-holder")) {
+ if (row.is('.js-temp-notes-holder')) {
// remove temporary row for diff lines
return row.remove();
} else {
@@ -965,7 +972,7 @@ const normalizeNewlines = function(str) {
Notes.prototype.cancelDiscussionForm = function(e) {
var form;
e.preventDefault();
- form = $(e.target).closest(".js-discussion-note-form");
+ form = $(e.target).closest('.js-discussion-note-form');
return this.removeDiscussionNoteForm(form);
};
@@ -977,10 +984,10 @@ const normalizeNewlines = function(str) {
Notes.prototype.updateFormAttachment = function() {
var filename, form;
- form = $(this).closest("form");
+ form = $(this).closest('form');
// get only the basename
- filename = $(this).val().replace(/^.*[\\\/]/, "");
- return form.find(".js-attachment-filename").text(filename);
+ filename = $(this).val().replace(/^.*[\\\/]/, '');
+ return form.find('.js-attachment-filename').text(filename);
};
/*
@@ -1212,7 +1219,7 @@ const normalizeNewlines = function(str) {
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
<div class="timeline-entry-inner">
<div class="timeline-icon">
- <a href="/${currentUsername}"><span class="dummy-avatar"></span></a>
+ <a href="/${currentUsername}"><span class="avatar dummy-avatar"></span></a>
</div>
<div class="timeline-content ${discussionClass}">
<div class="note-header">
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 14c98847d93..77cbaeb43ef 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,68 +1,32 @@
<script>
- /* global Flash */
- import Visibility from 'visibilityjs';
- import Poll from '../../../lib/utils/poll';
- import PipelineService from '../../services/pipeline_service';
- import PipelineStore from '../../stores/pipeline_store';
import stageColumnComponent from './stage_column_component.vue';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import '../../../flash';
export default {
+ props: {
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ },
+
components: {
stageColumnComponent,
loadingIcon,
},
- data() {
- const DOMdata = document.getElementById('js-pipeline-graph-vue').dataset;
- const store = new PipelineStore();
-
- return {
- isLoading: false,
- endpoint: DOMdata.endpoint,
- store,
- state: store.state,
- };
- },
-
- created() {
- this.service = new PipelineService(this.endpoint);
-
- const poll = new Poll({
- resource: this.service,
- method: 'getPipeline',
- successCallback: this.successCallback,
- errorCallback: this.errorCallback,
- });
-
- if (!Visibility.hidden()) {
- this.isLoading = true;
- poll.makeRequest();
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- poll.restart();
- } else {
- poll.stop();
- }
- });
+ computed: {
+ graph() {
+ return this.pipeline.details && this.pipeline.details.stages;
+ },
},
methods: {
- successCallback(response) {
- const data = response.json();
-
- this.isLoading = false;
- this.store.storeGraph(data.details.stages);
- },
-
- errorCallback() {
- this.isLoading = false;
- return new Flash('An error occurred while fetching the pipeline.');
- },
-
capitalizeStageName(name) {
return name.charAt(0).toUpperCase() + name.slice(1);
},
@@ -101,7 +65,7 @@
v-if="!isLoading"
class="stage-column-list">
<stage-column-component
- v-for="(stage, index) in state.graph"
+ v-for="(stage, index) in graph"
:title="capitalizeStageName(stage.name)"
:jobs="stage.groups"
:key="stage.name"
diff --git a/app/assets/javascripts/pipelines/graph_bundle.js b/app/assets/javascripts/pipelines/graph_bundle.js
deleted file mode 100644
index b7a6b5d8479..00000000000
--- a/app/assets/javascripts/pipelines/graph_bundle.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import Vue from 'vue';
-import pipelineGraph from './components/graph/graph_component.vue';
-
-document.addEventListener('DOMContentLoaded', () => new Vue({
- el: '#js-pipeline-graph-vue',
- components: {
- pipelineGraph,
- },
- render: createElement => createElement('pipeline-graph'),
-}));
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
new file mode 100644
index 00000000000..5aab25e0348
--- /dev/null
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import PipelinesMediator from './pipeline_details_mediatior';
+import pipelineGraph from './components/graph/graph_component.vue';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
+
+ const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
+
+ mediator.fetchPipeline();
+
+ const pipelineGraphApp = new Vue({
+ el: '#js-pipeline-graph-vue',
+ data() {
+ return {
+ mediator,
+ };
+ },
+ components: {
+ pipelineGraph,
+ },
+ render(createElement) {
+ return createElement('pipeline-graph', {
+ props: {
+ isLoading: this.mediator.state.isLoading,
+ pipeline: this.mediator.store.state.pipeline,
+ },
+ });
+ },
+ });
+
+ return pipelineGraphApp;
+});
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js
new file mode 100644
index 00000000000..b9a6d5ca5fc
--- /dev/null
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js
@@ -0,0 +1,51 @@
+/* global Flash */
+
+import Visibility from 'visibilityjs';
+import Poll from '../lib/utils/poll';
+import PipelineStore from './stores/pipeline_store';
+import PipelineService from './services/pipeline_service';
+
+export default class pipelinesMediator {
+ constructor(options = {}) {
+ this.options = options;
+ this.store = new PipelineStore();
+ this.service = new PipelineService(options.endpoint);
+
+ this.state = {};
+ this.state.isLoading = false;
+ }
+
+ fetchPipeline() {
+ this.poll = new Poll({
+ resource: this.service,
+ method: 'getPipeline',
+ successCallback: this.successCallback.bind(this),
+ errorCallback: this.errorCallback.bind(this),
+ });
+
+ if (!Visibility.hidden()) {
+ this.state.isLoading = true;
+ this.poll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+ }
+
+ successCallback(response) {
+ const data = response.json();
+
+ this.state.isLoading = false;
+ this.store.storePipeline(data);
+ }
+
+ errorCallback() {
+ this.state.isLoading = false;
+ return new Flash('An error occurred while fetching the pipeline.');
+ }
+}
diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js
index 86ab50d8f1e..052e34a8aef 100644
--- a/app/assets/javascripts/pipelines/stores/pipeline_store.js
+++ b/app/assets/javascripts/pipelines/stores/pipeline_store.js
@@ -2,10 +2,10 @@ export default class PipelineStore {
constructor() {
this.state = {};
- this.state.graph = [];
+ this.state.pipeline = {};
}
- storeGraph(graph = []) {
- this.state.graph = graph;
+ storePipeline(pipeline = {}) {
+ this.state.pipeline = pipeline;
}
}
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index bacb26734c9..c44892dae3d 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -4,7 +4,7 @@
window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
- WRAPPER = '<div class="diff-content diff-wrap-lines"></div>';
+ WRAPPER = '<div class="diff-content"></div>';
LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index aea3592c6ba..ec45253e50b 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -35,6 +35,7 @@ function UsersSelect(currentUser, els) {
options.showCurrentUser = $dropdown.data('current-user');
options.todoFilter = $dropdown.data('todo-filter');
options.todoStateFilter = $dropdown.data('todo-state-filter');
+ options.perPage = $dropdown.data('per-page');
showNullUser = $dropdown.data('null-user');
defaultNullUser = $dropdown.data('null-user-default');
showMenuAbove = $dropdown.data('showMenuAbove');
@@ -214,7 +215,36 @@ function UsersSelect(currentUser, els) {
glDropdown.options.processData(term, users, callback);
}.bind(this));
},
- processData: function(term, users, callback) {
+ processData: function(term, data, callback) {
+ let users = data;
+
+ // Only show assigned user list when there is no search term
+ if ($dropdown.hasClass('js-multiselect') && term.length === 0) {
+ const selectedInputs = getSelectedUserInputs();
+
+ // Potential duplicate entries when dealing with issue board
+ // because issue board is also managed by vue
+ const selectedUsers = _.uniq(selectedInputs, false, a => a.value)
+ .filter((input) => {
+ const userId = parseInt(input.value, 10);
+ const inUsersArray = users.find(u => u.id === userId);
+
+ return !inUsersArray && userId !== 0;
+ })
+ .map((input) => {
+ const userId = parseInt(input.value, 10);
+ const { avatarUrl, avatar_url, name, username } = input.dataset;
+ return {
+ avatar_url: avatarUrl || avatar_url,
+ id: userId,
+ name,
+ username,
+ };
+ });
+
+ users = data.concat(selectedUsers);
+ }
+
let anyUser;
let index;
let j;
@@ -645,7 +675,7 @@ UsersSelect.prototype.users = function(query, options, callback) {
url: url,
data: {
search: query,
- per_page: 20,
+ per_page: options.perPage || 20,
active: true,
project_id: options.projectId || null,
group_id: options.groupId || null,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
index 486b13e60af..8155218681c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
@@ -1,4 +1,6 @@
import statusCodes from '~/lib/utils/http_status';
+import { bytesToMiB } from '~/lib/utils/number_utils';
+
import MemoryGraph from '../../vue_shared/components/memory_graph';
import MRWidgetService from '../services/mr_widget_service';
@@ -9,8 +11,8 @@ export default {
},
data() {
return {
- // memoryFrom: 0,
- // memoryTo: 0,
+ memoryFrom: 0,
+ memoryTo: 0,
memoryMetrics: [],
deploymentTime: 0,
hasMetrics: false,
@@ -35,18 +37,38 @@ export default {
shouldShowMetricsUnavailable() {
return !this.loadingMetrics && !this.hasMetrics && !this.loadFailed;
},
+ memoryChangeType() {
+ const memoryTo = Number(this.memoryTo);
+ const memoryFrom = Number(this.memoryFrom);
+
+ if (memoryTo > memoryFrom) {
+ return 'increased';
+ } else if (memoryTo < memoryFrom) {
+ return 'decreased';
+ }
+
+ return 'unchanged';
+ },
},
methods: {
+ getMegabytes(bytesString) {
+ const valueInBytes = Number(bytesString).toFixed(2);
+ return (bytesToMiB(valueInBytes)).toFixed(2);
+ },
computeGraphData(metrics, deploymentTime) {
this.loadingMetrics = false;
- const { memory_values } = metrics;
- // if (memory_previous.length > 0) {
- // this.memoryFrom = Number(memory_previous[0].value[1]).toFixed(2);
- // }
- //
- // if (memory_current.length > 0) {
- // this.memoryTo = Number(memory_current[0].value[1]).toFixed(2);
- // }
+ const { memory_before, memory_after, memory_values } = metrics;
+
+ // Both `memory_before` and `memory_after` objects
+ // have peculiar structure where accessing only a specific
+ // index yeilds correct value that we can use to show memory delta.
+ if (memory_before.length > 0) {
+ this.memoryFrom = this.getMegabytes(memory_before[0].value[1]);
+ }
+
+ if (memory_after.length > 0) {
+ this.memoryTo = this.getMegabytes(memory_after[0].value[1]);
+ }
if (memory_values.length > 0) {
this.hasMetrics = true;
@@ -102,7 +124,7 @@ export default {
<p
v-if="shouldShowMemoryGraph"
class="usage-info js-usage-info">
- Deployment memory usage:
+ Memory usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB
</p>
<p
v-if="shouldShowLoadFailure"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index d866d4e94b0..fcd4fdaf09f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -13,7 +13,7 @@ export default {
},
data() {
return {
- removeSourceBranch: true,
+ removeSourceBranch: this.mr.shouldRemoveSourceBranch,
mergeWhenBuildSucceeds: false,
useCommitMessageWithDescription: false,
setToMergeWhenPipelineSucceeds: false,
@@ -69,6 +69,9 @@ export default {
|| this.isMakingRequest
|| this.mr.preventMerge);
},
+ isRemoveSourceBranchButtonDisabled() {
+ return this.isMergeButtonDisabled || !this.mr.canRemoveSourceBranch;
+ },
shouldShowSquashBeforeMerge() {
const { commitsCount, enableSquashBeforeMerge } = this.mr;
return enableSquashBeforeMerge && commitsCount > 1;
@@ -252,8 +255,9 @@ export default {
<template v-if="isMergeAllowed()">
<label class="spacing">
<input
+ id="remove-source-branch-input"
v-model="removeSourceBranch"
- :disabled="isMergeButtonDisabled"
+ :disabled="isRemoveSourceBranchButtonDisabled"
type="checkbox"/> Remove source branch
</label>
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 c07bd25e6fd..dc4b2195050 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
@@ -50,7 +50,7 @@ export default class MergeRequestStore {
this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path;
this.removeWIPPath = data.remove_wip_path;
this.sourceBranchRemoved = !data.source_branch_exists;
- this.shouldRemoveSourceBranch = (data.merge_params || {}).should_remove_source_branch || false;
+ this.shouldRemoveSourceBranch = data.remove_source_branch || false;
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false;
this.mergePath = data.merge_path;
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
new file mode 100644
index 00000000000..fd0dcd716d6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -0,0 +1,122 @@
+<script>
+import ciIconBadge from './ci_badge_link.vue';
+import timeagoTooltip from './time_ago_tooltip.vue';
+import tooltipMixin from '../mixins/tooltip';
+import userAvatarLink from './user_avatar/user_avatar_link.vue';
+
+/**
+ * Renders header component for job and pipeline page based on UI mockups
+ *
+ * Used in:
+ * - job show page
+ * - pipeline show page
+ */
+export default {
+ props: {
+ status: {
+ type: Object,
+ required: true,
+ },
+ itemName: {
+ type: String,
+ required: true,
+ },
+ itemId: {
+ type: Number,
+ required: true,
+ },
+ time: {
+ type: String,
+ required: true,
+ },
+ user: {
+ type: Object,
+ required: true,
+ },
+ actions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+
+ mixins: [
+ tooltipMixin,
+ ],
+
+ components: {
+ ciIconBadge,
+ timeagoTooltip,
+ userAvatarLink,
+ },
+
+ computed: {
+ userAvatarAltText() {
+ return `${this.user.name}'s avatar`;
+ },
+ },
+
+ methods: {
+ onClickAction(action) {
+ this.$emit('postAction', action);
+ },
+ },
+};
+</script>
+<template>
+ <header class="page-content-header top-area">
+ <section class="header-main-content">
+
+ <ci-icon-badge :status="status" />
+
+ <strong>
+ {{itemName}} #{{itemId}}
+ </strong>
+
+ triggered
+
+ <timeago-tooltip :time="time" />
+
+ by
+
+ <user-avatar-link
+ :link-href="user.web_url"
+ :img-src="user.avatar_url"
+ :img-alt="userAvatarAltText"
+ :tooltip-text="user.name"
+ :img-size="24"
+ />
+
+ <a
+ :href="user.web_url"
+ :title="user.email"
+ class="js-user-link commit-committer-link"
+ ref="tooltip">
+ {{user.name}}
+ </a>
+ </section>
+
+ <section
+ class="header-action-button nav-controls"
+ v-if="actions.length">
+ <template
+ v-for="action in actions">
+ <a
+ v-if="action.type === 'link'"
+ :href="action.path"
+ :class="action.cssClass">
+ {{action.label}}
+ </a>
+
+ <button
+ v-else="action.type === 'button'"
+ @click="onClickAction(action)"
+ :class="action.cssClass"
+ type="button">
+ {{action.label}}
+ </button>
+
+ </template>
+ </section>
+ </header>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
new file mode 100644
index 00000000000..af2b4c6786e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
@@ -0,0 +1,58 @@
+<script>
+import tooltipMixin from '../mixins/tooltip';
+import timeagoMixin from '../mixins/timeago';
+import '../../lib/utils/datetime_utility';
+
+/**
+ * Port of ruby helper time_ago_with_tooltip
+ */
+
+export default {
+ props: {
+ time: {
+ type: String,
+ required: true,
+ },
+
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+
+ shortFormat: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ cssClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ mixins: [
+ tooltipMixin,
+ timeagoMixin,
+ ],
+
+ computed: {
+ timeagoCssClass() {
+ return this.shortFormat ? 'js-short-timeago' : 'js-timeago';
+ },
+ },
+};
+</script>
+<template>
+ <time
+ :class="[timeagoCssClass, cssClass]"
+ class="js-timeago js-timeago-render"
+ :title="tooltipTitle(time)"
+ :data-placement="tooltipPlacement"
+ data-container="body"
+ ref="tooltip">
+ {{timeFormated(time)}}
+ </time>
+</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js
new file mode 100644
index 00000000000..20f63ab663c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/mixins/timeago.js
@@ -0,0 +1,18 @@
+import '../../lib/utils/datetime_utility';
+
+/**
+ * Mixin with time ago methods used in some vue components
+ */
+export default {
+ methods: {
+ timeFormated(time) {
+ const timeago = gl.utils.getTimeago();
+
+ return timeago.format(time);
+ },
+
+ tooltipTitle(time) {
+ return gl.utils.formatDate(time);
+ },
+ },
+};
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index d2ec1791d2b..b8ba77f4513 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -34,6 +34,7 @@
@import "framework/selects.scss";
@import "framework/sidebar.scss";
@import "framework/tables.scss";
+@import "framework/notes.scss";
@import "framework/timeline.scss";
@import "framework/typography.scss";
@import "framework/zen.scss";
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index d64b1237b2c..75907c35b7e 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -10,7 +10,7 @@
top: 0;
margin-top: 3px;
padding: $gl-padding;
- z-index: 9;
+ z-index: 300;
width: 300px;
font-size: 14px;
background-color: $white-light;
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 3dec911d289..fefe5575d9b 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -23,7 +23,6 @@
.row-content-block {
margin-top: 0;
- margin-bottom: -$gl-padding;
background-color: $gray-light;
padding: $gl-padding;
margin-bottom: 0;
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index d86ae57cd9a..2d6bc17d4ff 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -1,5 +1,4 @@
gl-emoji {
- display: inline-block;
display: inline-flex;
vertical-align: middle;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index f8674b763c8..78f425057eb 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -66,10 +66,10 @@
&.video {
background: $file-image-bg;
text-align: center;
+ padding: 30px;
img,
video {
- padding: 20px;
max-width: 80%;
}
}
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index eadb9409fee..25b4feca3c3 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -36,6 +36,10 @@
border-radius: 0;
}
}
+
+ &:empty {
+ margin: 0;
+ }
}
@media (max-width: $screen-sm-max) {
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index d76053fe72a..49163653548 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -11,7 +11,6 @@
> li {
padding: 10px 15px;
min-height: 20px;
- border-bottom: 1px solid $list-border-light;
border-bottom: 1px solid $list-border;
&::after {
diff --git a/app/assets/stylesheets/framework/notes.scss b/app/assets/stylesheets/framework/notes.scss
new file mode 100644
index 00000000000..d349e3fad9c
--- /dev/null
+++ b/app/assets/stylesheets/framework/notes.scss
@@ -0,0 +1,14 @@
+@mixin notes-media($condition, $breakpoint-width) {
+ @media (#{$condition}-width: ($breakpoint-width)) {
+ @content;
+ }
+
+ // Diff is side by side
+ .notes_content.parallel & {
+ // We hide at double what we normally hide at because
+ // there are two columns of notes
+ @media (#{$condition}-width: (2 * $breakpoint-width)) {
+ @content;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 9ab17e67d4c..5ae833cd5f6 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -96,7 +96,6 @@
.select2-search-field input {
padding: 5px $gl-padding / 2;
- font-size: 13px;
height: auto;
font-family: inherit;
font-size: inherit;
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 1fd734d279b..cec3b54d567 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -3,9 +3,9 @@
margin: 0;
padding: 0;
- .note-text {
- p:last-child {
- margin-bottom: 0 !important;
+ &::before {
+ @include notes-media('max', $screen-xs-max) {
+ background: none;
}
}
@@ -29,6 +29,16 @@
.timeline-entry-inner {
position: relative;
+
+ @include notes-media('max', $screen-xs-max) {
+ .timeline-icon {
+ display: none;
+ }
+
+ .timeline-content {
+ margin-left: 0;
+ }
+ }
}
&:target,
@@ -46,24 +56,6 @@
}
}
-@media (max-width: $screen-xs-max) {
- .timeline {
- &::before {
- background: none;
- }
- }
-
- .timeline-entry .timeline-entry-inner {
- .timeline-icon {
- display: none;
- }
-
- .timeline-content {
- margin-left: 0;
- }
- }
-}
-
.discussion .timeline-entry {
margin: 0;
border-right: none;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 0c3407f34f8..785b09e622f 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -21,6 +21,10 @@
margin-top: 0;
}
+ > :last-child {
+ margin-bottom: 0;
+ }
+
// Single code lines should wrap
code {
font-family: $monospace_font;
@@ -157,7 +161,7 @@
ul,
ol {
padding: 0;
- margin: 0 0 16px !important;
+ margin: 0 0 16px;
}
ul:dir(rtl),
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 17a4e8fd83e..4db77752c0c 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -247,7 +247,6 @@ $dark-diff-match-bg: rgba(255, 255, 255, 0.3);
$dark-diff-match-color: rgba(255, 255, 255, 0.1);
$file-mode-changed: #777;
$file-mode-changed: #777;
-$diff-image-bg: #ddd;
$diff-image-info-color: grey;
$diff-swipe-border: #999;
$diff-view-modes-color: grey;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 68d7ab4bf84..ebe662136d5 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -72,7 +72,9 @@
@media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
+ // scss-lint:disable DuplicateProperty
height: calc(100vh - 222px);
+ // scss-lint:enable DuplicateProperty
min-height: 475px;
transition: width .2s;
diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss
index 90643832390..7b4eb689f1b 100644
--- a/app/assets/stylesheets/pages/ci_projects.scss
+++ b/app/assets/stylesheets/pages/ci_projects.scss
@@ -36,7 +36,6 @@
pre.commit-message {
background: none;
padding: 0;
- margin: 0;
border: none;
margin: 20px 0;
border-radius: 0;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index cfb1df4df84..b58922626fa 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -94,7 +94,6 @@
.old_line,
.new_line {
margin: 0;
- padding: 0;
border: none;
padding: 0 5px;
border-right: 1px solid;
@@ -151,14 +150,10 @@
}
}
}
-
- .text-file.diff-wrap-lines table .line_holder td span {
- white-space: pre-wrap;
- }
}
.image {
- background: $diff-image-bg;
+ background: $file-image-bg;
text-align: center;
padding: 30px;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 48d3b7b1d07..f269d53093d 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -64,6 +64,10 @@
}
}
+ .btn .text-center {
+ display: inline;
+ }
+
.commit-title {
margin: 0;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index d79ae47f589..c2346f2f1c3 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -431,7 +431,7 @@
}
.detail-page-description {
- padding: 16px 0 0;
+ padding: 16px 0;
small {
color: $gray-darkest;
@@ -441,7 +441,7 @@
.edited-text {
color: $gray-darkest;
display: block;
- margin: 0 0 16px;
+ margin: 16px 0 0;
.author_link {
color: $gray-darkest;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index bee9b13b375..702e7662527 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -204,7 +204,6 @@ ul.related-merge-requests > li {
.dropdown-toggle {
.fa-caret-down {
pointer-events: none;
- margin-left: 0;
color: inherit;
margin-left: 0;
}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 8dbac76e30a..971d54e7472 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -184,4 +184,4 @@
}
}
}
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 49e453c7dbc..875e47cdff3 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -28,7 +28,7 @@
.note-edit-form {
.note-form-actions {
position: relative;
- margin: $gl-padding 0;
+ margin: $gl-padding 0 0;
}
.note-preview-holder {
@@ -124,10 +124,18 @@
}
.discussion-form {
- padding: $gl-padding-top $gl-padding;
+ padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light;
}
+.discussion-notes .disabled-comment {
+ padding: 6px 0;
+}
+
+.notes-form > li {
+ border: 0;
+}
+
.note-edit-form {
display: none;
font-size: 14px;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 51918917329..cffd3b6060d 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -14,24 +14,11 @@ ul.notes {
margin: 0;
padding: 0;
- .timeline-icon {
- float: left;
-
- svg {
- width: 16px;
- height: 16px;
- fill: $gray-darkest;
- position: absolute;
- left: 0;
- top: 16px;
- }
- }
-
.timeline-content {
margin-left: 55px;
&.timeline-content-form {
- @media (max-width: $screen-sm-max) {
+ @include notes-media('max', $screen-sm-max) {
margin-left: 0;
}
}
@@ -56,21 +43,22 @@ ul.notes {
position: relative;
}
- .note {
- padding: $gl-padding $gl-btn-padding 0;
+ > li {
+ padding: $gl-padding $gl-btn-padding;
display: block;
position: relative;
border-bottom: 1px solid $white-normal;
+ &:last-child {
+ // Override `.timeline > li:last-child { border-bottom: none; }`
+ border-bottom: 1px solid $white-normal;
+ }
+
&.being-posted {
pointer-events: none;
opacity: 0.5;
.dummy-avatar {
- display: inline-block;
- height: 40px;
- width: 40px;
- border-radius: 50%;
background-color: $kdb-border;
border: 1px solid darken($kdb-border, 25%);
}
@@ -126,13 +114,13 @@ ul.notes {
.note-awards {
.js-awards-block {
- margin-bottom: 16px;
+ margin-top: 16px;
}
}
.note-header {
- @media (max-width: $screen-xs-min) {
+ @include notes-media('max', $screen-xs-min) {
.inline {
display: block;
}
@@ -161,10 +149,10 @@ ul.notes {
.system-note {
font-size: 14px;
- padding: 0;
+ padding-left: 0;
clear: both;
- @media (min-width: $screen-sm-min) {
+ @include notes-media('min', $screen-sm-min) {
margin-left: 65px;
}
@@ -198,11 +186,22 @@ ul.notes {
}
}
- .timeline-content {
- padding: 14px 10px;
+ .timeline-icon {
+ float: left;
- @media (min-width: $screen-sm-min) {
- margin-left: 20px;
+ svg {
+ width: 16px;
+ height: 16px;
+ fill: $gray-darkest;
+ position: absolute;
+ left: 0;
+ top: 2px;
+ }
+ }
+
+ .timeline-content {
+ @include notes-media('min', $screen-sm-min) {
+ margin-left: 30px;
}
}
@@ -371,7 +370,7 @@ ul.notes {
display: flex;
justify-content: space-between;
- @media (max-width: $screen-xs-max) {
+ @include notes-media('max', $screen-xs-max) {
flex-flow: row wrap;
}
}
@@ -385,10 +384,16 @@ ul.notes {
padding-bottom: 0;
}
+.note-header-author-name {
+ @include notes-media('max', $screen-xs-max) {
+ display: none;
+ }
+}
+
.note-headline-light {
display: inline;
- @media (max-width: $screen-xs-min) {
+ @include notes-media('max', $screen-xs-min) {
display: block;
}
}
@@ -430,7 +435,7 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
- @media (max-width: $screen-xs-max) {
+ @include notes-media('max', $screen-xs-max) {
float: none;
margin-left: 0;
}
@@ -441,7 +446,7 @@ ul.notes {
}
.discussion-actions {
- @media (max-width: $screen-md-max) {
+ @include notes-media('max', $screen-md-max) {
float: none;
margin-left: 0;
@@ -455,7 +460,7 @@ ul.notes {
display: inline;
line-height: 20px;
- @media (min-width: $screen-sm-min) {
+ @include notes-media('min', $screen-sm-min) {
margin-left: 10px;
line-height: 24px;
}
@@ -590,10 +595,15 @@ ul.notes {
.discussion-body,
.diff-file {
.notes .note {
- padding: 10px 15px;
+ padding-left: $gl-padding;
+ padding-right: $gl-padding;
&.system-note {
- padding: 0;
+ padding-left: 0;
+
+ @media (min-width: $screen-sm-min) {
+ margin-left: 70px;
+ }
}
}
}
@@ -607,17 +617,11 @@ ul.notes {
}
.disabled-comment {
- margin-left: -$gl-padding-top;
- margin-right: -$gl-padding-top;
background-color: $gray-light;
border-radius: $border-radius-base;
border: 1px solid $border-gray-normal;
color: $note-disabled-comment-color;
- line-height: 200px;
-
- .disabled-comment-text {
- line-height: normal;
- }
+ padding: 90px 0;
a {
color: $gl-link-color;
@@ -625,7 +629,7 @@ ul.notes {
}
.line-resolve-all-container {
- @media (min-width: $screen-sm-min) {
+ @include notes-media('min', $screen-sm-min) {
margin-right: 0;
padding-left: $gl-padding;
}
@@ -667,7 +671,7 @@ ul.notes {
.line-resolve-all {
vertical-align: middle;
display: inline-block;
- padding: 6px 10px;
+ padding: 5px 10px 6px;
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
@@ -680,6 +684,10 @@ ul.notes {
.line-resolve-btn {
margin-right: 5px;
+
+ svg {
+ vertical-align: middle;
+ }
}
}
@@ -716,6 +724,10 @@ ul.notes {
}
}
+.line-resolve-text {
+ vertical-align: middle;
+}
+
.discussion-next-btn {
svg {
margin: 0;
@@ -732,11 +744,6 @@ ul.notes {
// Merge request notes in diffs
.diff-file {
- // Diff is side by side
- .notes_content.parallel .note-header .note-headline-light {
- display: block;
- position: relative;
- }
// Diff is inline
.notes_content .note-header .note-headline-light {
display: inline-block;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 292584eba28..cf2e565dd2d 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -88,6 +88,10 @@
}
}
+ .btn .text-center {
+ display: inline;
+ }
+
.tooltip {
white-space: nowrap;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index f0bf3d4c267..24ab2bedea2 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -247,7 +247,6 @@
font-size: 13px;
font-weight: 600;
line-height: 13px;
- padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px;
padding: 6px 14px;
text-align: center;
@@ -384,10 +383,6 @@ a.deploy-project-label {
}
}
-.last-push-widget {
- margin-top: -1px;
-}
-
.fork-namespaces {
.row {
-webkit-flex-wrap: wrap;
diff --git a/app/controllers/admin/hook_logs_controller.rb b/app/controllers/admin/hook_logs_controller.rb
new file mode 100644
index 00000000000..aa069b89563
--- /dev/null
+++ b/app/controllers/admin/hook_logs_controller.rb
@@ -0,0 +1,29 @@
+class Admin::HookLogsController < Admin::ApplicationController
+ include HooksExecution
+
+ before_action :hook, only: [:show, :retry]
+ before_action :hook_log, only: [:show, :retry]
+
+ respond_to :html
+
+ def show
+ end
+
+ def retry
+ status, message = hook.execute(hook_log.request_data, hook_log.trigger)
+
+ set_hook_execution_notice(status, message)
+
+ redirect_to edit_admin_hook_path(@hook)
+ end
+
+ private
+
+ def hook
+ @hook ||= SystemHook.find(params[:hook_id])
+ end
+
+ def hook_log
+ @hook_log ||= hook.web_hook_logs.find(params[:id])
+ end
+end
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index ccfe553c89e..b9251e140f8 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -1,5 +1,7 @@
class Admin::HooksController < Admin::ApplicationController
- before_action :hook, only: :edit
+ include HooksExecution
+
+ before_action :hook_logs, only: :edit
def index
@hooks = SystemHook.all
@@ -36,15 +38,9 @@ class Admin::HooksController < Admin::ApplicationController
end
def test
- data = {
- event_name: "project_create",
- name: "Ruby",
- path: "ruby",
- project_id: 1,
- owner_name: "Someone",
- owner_email: "example@gitlabhq.com"
- }
- hook.execute(data, 'system_hooks')
+ status, message = hook.execute(sample_hook_data, 'system_hooks')
+
+ set_hook_execution_notice(status, message)
redirect_back_or_default
end
@@ -55,6 +51,11 @@ class Admin::HooksController < Admin::ApplicationController
@hook ||= SystemHook.find(params[:id])
end
+ def hook_logs
+ @hook_logs ||=
+ Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
+ end
+
def hook_params
params.require(:hook).permit(
:enable_ssl_verification,
@@ -65,4 +66,15 @@ class Admin::HooksController < Admin::ApplicationController
:url
)
end
+
+ def sample_hook_data
+ {
+ event_name: "project_create",
+ name: "Ruby",
+ path: "ruby",
+ project_id: 1,
+ owner_name: "Someone",
+ owner_email: "example@gitlabhq.com"
+ }
+ end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index ab5aed24917..47ce21d238b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -283,12 +283,8 @@ class ApplicationController < ActionController::Base
request.base_url
end
- def set_locale
- Gitlab::I18n.set_locale(current_user)
-
- yield
- ensure
- Gitlab::I18n.reset_locale
+ def set_locale(&block)
+ Gitlab::I18n.with_user_locale(current_user, &block)
end
def sessionless_sign_in(user)
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index e2f5aa8508e..907717dcb96 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -9,7 +9,7 @@ class AutocompleteController < ApplicationController
@users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present?
@users = @users.active
@users = @users.reorder(:name)
- @users = @users.page(params[:page])
+ @users = @users.page(params[:page]).per(params[:per_page])
if params[:todo_filter].present? && current_user
@users = @users.todo_authors(current_user.id, params[:todo_state_filter])
diff --git a/app/controllers/concerns/diff_for_path.rb b/app/controllers/concerns/diff_for_path.rb
index 1efa9fe060f..d5388c4cd20 100644
--- a/app/controllers/concerns/diff_for_path.rb
+++ b/app/controllers/concerns/diff_for_path.rb
@@ -8,17 +8,6 @@ module DiffForPath
return render_404 unless diff_file
- diff_commit = commit_for_diff(diff_file)
- blob = diff_file.blob(diff_commit)
-
- locals = {
- diff_file: diff_file,
- diff_commit: diff_commit,
- diff_refs: diffs.diff_refs,
- blob: blob,
- project: project
- }
-
- render json: { html: view_to_html_string('projects/diffs/_content', locals) }
+ render json: { html: view_to_html_string('projects/diffs/_content', diff_file: diff_file) }
end
end
diff --git a/app/controllers/concerns/hooks_execution.rb b/app/controllers/concerns/hooks_execution.rb
new file mode 100644
index 00000000000..846cd60518f
--- /dev/null
+++ b/app/controllers/concerns/hooks_execution.rb
@@ -0,0 +1,15 @@
+module HooksExecution
+ extend ActiveSupport::Concern
+
+ private
+
+ def set_hook_execution_notice(status, message)
+ if status && status >= 200 && status < 400
+ flash[:notice] = "Hook executed successfully: HTTP #{status}"
+ elsif status
+ flash[:alert] = "Hook executed successfully but returned HTTP #{status} #{message}"
+ else
+ flash[:alert] = "Hook execution failed: #{message}"
+ end
+ end
+end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 5a1efcab1a3..3d49ea97591 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -8,7 +8,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = load_projects(params.merge(non_public: true)).page(params[:page])
respond_to do |format|
- format.html { @last_push = current_user.recent_push }
+ format.html
format.atom do
load_events
render layout: false
@@ -25,7 +25,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = load_projects(params.merge(starred: true)).
includes(:forked_from_project, :tags).page(params[:page])
- @last_push = current_user.recent_push
@groups = []
respond_to do |format|
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 79d420a32d3..6195121b931 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -9,8 +9,6 @@ class DashboardController < Dashboard::ApplicationController
respond_to :html
def activity
- @last_push = current_user.recent_push
-
respond_to do |format|
format.html
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 965ced4d372..18a2d69db29 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -64,6 +64,8 @@ class GroupsController < Groups::ApplicationController
end
def subgroups
+ return not_found unless Group.supports_nested_groups?
+
@nested_groups = GroupsFinder.new(current_user, parent: group).execute
@nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
end
@@ -165,7 +167,6 @@ class GroupsController < Groups::ApplicationController
def user_actions
if current_user
- @last_push = current_user.recent_push
@notification_setting = current_user.notification_settings_for(group)
end
end
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 008d2f5815f..88dd600e5fe 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -51,13 +51,9 @@ class Projects::CompareController < Projects::ApplicationController
if @compare
@commits = @compare.commits
- @start_commit = @compare.start_commit
- @commit = @compare.commit
- @base_commit = @compare.base_commit
-
@diffs = @compare.diffs(diff_options)
- environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @commit }
+ environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @compare.commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@diff_notes_disabled = true
diff --git a/app/controllers/projects/hook_logs_controller.rb b/app/controllers/projects/hook_logs_controller.rb
new file mode 100644
index 00000000000..354f0d6db3a
--- /dev/null
+++ b/app/controllers/projects/hook_logs_controller.rb
@@ -0,0 +1,33 @@
+class Projects::HookLogsController < Projects::ApplicationController
+ include HooksExecution
+
+ before_action :authorize_admin_project!
+
+ before_action :hook, only: [:show, :retry]
+ before_action :hook_log, only: [:show, :retry]
+
+ respond_to :html
+
+ layout 'project_settings'
+
+ def show
+ end
+
+ def retry
+ status, message = hook.execute(hook_log.request_data, hook_log.trigger)
+
+ set_hook_execution_notice(status, message)
+
+ redirect_to edit_namespace_project_hook_path(@project.namespace, @project, @hook)
+ end
+
+ private
+
+ def hook
+ @hook ||= @project.hooks.find(params[:hook_id])
+ end
+
+ def hook_log
+ @hook_log ||= hook.web_hook_logs.find(params[:id])
+ end
+end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index 86d13a0d222..38bd82841dc 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -1,7 +1,9 @@
class Projects::HooksController < Projects::ApplicationController
+ include HooksExecution
+
# Authorize
before_action :authorize_admin_project!
- before_action :hook, only: :edit
+ before_action :hook_logs, only: :edit
respond_to :html
@@ -34,13 +36,7 @@ class Projects::HooksController < Projects::ApplicationController
if !@project.empty_repo?
status, message = TestHookService.new.execute(hook, current_user)
- if status && status >= 200 && status < 400
- flash[:notice] = "Hook executed successfully: HTTP #{status}"
- elsif status
- flash[:alert] = "Hook executed successfully but returned HTTP #{status} #{message}"
- else
- flash[:alert] = "Hook execution failed: #{message}"
- end
+ set_hook_execution_notice(status, message)
else
flash[:alert] = 'Hook execution failed. Ensure the project has commits.'
end
@@ -60,6 +56,11 @@ class Projects::HooksController < Projects::ApplicationController
@hook ||= @project.hooks.find(params[:id])
end
+ def hook_logs
+ @hook_logs ||=
+ Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
+ end
+
def hook_params
params.require(:hook).permit(
:job_events,
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 0352065998b..314906b5f09 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -14,7 +14,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
- before_action :define_commit_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :check_if_can_be_merged, only: :show
@@ -130,8 +129,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@diff_notes_disabled = true
end
- define_commit_vars
-
render_diff_for_path(@diffs)
end
@@ -500,11 +497,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
end
- def define_commit_vars
- @commit = @merge_request.diff_head_commit
- @base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit
- end
-
def define_diff_vars
@merge_request_diff =
if params[:diff_id]
@@ -569,7 +561,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
- @base_commit = @merge_request.diff_base_commit
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 667f4870c7a..2a0b58fae7c 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -74,6 +74,6 @@ class Projects::RefsController < Projects::ApplicationController
private
def validate_ref_id
- return not_found! if params[:id].present? && params[:id] !~ Gitlab::Regex.git_reference_regex
+ return not_found! if params[:id].present? && params[:id] !~ Gitlab::PathRegex.git_reference_regex
end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index d59d51905a6..5b5cdebe919 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -15,16 +15,6 @@ module CommitsHelper
commit_person_link(commit, options.merge(source: :committer))
end
- def image_diff_class(diff)
- if diff.deleted_file
- "deleted"
- elsif diff.new_file
- "added"
- else
- nil
- end
- end
-
def commit_to_html(commit, ref, project)
render 'projects/commits/commit',
commit: commit,
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 4a06ee653ee..4c4fbdd4d39 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -102,14 +102,14 @@ module DiffHelper
].join(' ').html_safe
end
- def commit_for_diff(diff_file)
- return diff_file.content_commit if diff_file.content_commit
+ def diff_file_blob_raw_path(diff_file)
+ namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.content_sha, diff_file.file_path))
+ end
- if diff_file.deleted_file
- @base_commit || @commit.parent || @commit
- else
- @commit
- end
+ def diff_file_old_blob_raw_path(diff_file)
+ sha = diff_file.old_content_sha
+ return unless sha
+ namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.old_content_sha, diff_file.old_path))
end
def diff_file_html_data(project, diff_file_path, diff_commit_id)
@@ -120,8 +120,8 @@ module DiffHelper
}
end
- def editable_diff?(diff)
- !diff.deleted_file && @merge_request && @merge_request.source_project
+ def editable_diff?(diff_file)
+ !diff_file.deleted_file? && @merge_request && @merge_request.source_project
end
private
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index e5b1e6e8bc7..4e6e6805920 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -69,13 +69,12 @@ module LabelsHelper
end
def render_colored_label(label, label_suffix = '', tooltip: true)
- label_color = label.color || Label::DEFAULT_COLOR
- text_color = text_color_for_bg(label_color)
+ text_color = text_color_for_bg(label.color)
# Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter
span = %(<span class="label color-label #{"has-tooltip" if tooltip}" ) +
- %(style="background-color: #{label_color}; color: #{text_color}" ) +
+ %(style="background-color: #{label.color}; color: #{text_color}" ) +
%(title="#{escape_once(label.description)}" data-container="body">) +
%(#{escape_once(label.name)}#{label_suffix}</span>)
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 98bbcfaaba5..654aa1a6533 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -85,6 +85,12 @@ module ProjectsHelper
@nav_tabs ||= get_project_nav_tabs(@project, current_user)
end
+ def project_search_tabs?(tab)
+ abilities = Array(search_tab_ability_map[tab])
+
+ abilities.any? { |ability| can?(current_user, ability, @project) }
+ end
+
def project_nav_tab?(name)
project_nav_tabs.include? name
end
@@ -116,6 +122,7 @@ module ProjectsHelper
def last_push_event
return unless current_user
+ return current_user.recent_push unless @project
project_ids = [@project.id]
if fork = current_user.fork_of(@project)
@@ -203,7 +210,17 @@ module ProjectsHelper
nav_tabs << :container_registry
end
- tab_ability_map = {
+ tab_ability_map.each do |tab, ability|
+ if can?(current_user, ability, project)
+ nav_tabs << tab
+ end
+ end
+
+ nav_tabs.flatten
+ end
+
+ def tab_ability_map
+ {
environments: :read_environment,
milestones: :read_milestone,
pipelines: :read_pipeline,
@@ -215,14 +232,15 @@ module ProjectsHelper
team: :read_project_member,
wiki: :read_wiki
}
+ end
- tab_ability_map.each do |tab, ability|
- if can?(current_user, ability, project)
- nav_tabs << tab
- end
- end
-
- nav_tabs.flatten
+ def search_tab_ability_map
+ @search_tab_ability_map ||= tab_ability_map.merge(
+ blobs: :download_code,
+ commits: :download_code,
+ merge_requests: :read_merge_request,
+ notes: [:read_merge_request, :download_code, :read_issue, :read_project_snippet]
+ )
end
def project_lfs_status(project)
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index 09b73eee8cf..c0763a8a9c4 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -13,6 +13,7 @@ module SubmoduleHelper
if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
namespace, project = $1, $2
+ project.rstrip!
project.sub!(/\.git\z/, '')
if self_url?(url, namespace, project)
diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb
index d2980db218a..654468bc7fe 100644
--- a/app/mailers/base_mailer.rb
+++ b/app/mailers/base_mailer.rb
@@ -1,4 +1,6 @@
class BaseMailer < ActionMailer::Base
+ around_action :render_with_default_locale
+
helper ApplicationHelper
helper MarkupHelper
@@ -14,6 +16,10 @@ class BaseMailer < ActionMailer::Base
private
+ def render_with_default_locale(&block)
+ Gitlab::I18n.with_default_locale(&block)
+ end
+
def default_sender_address
address = Mail::Address.new(Gitlab.config.gitlab.email_from)
address.display_name = Gitlab.config.gitlab.email_display_name
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index cf6e53c4ca4..07213ca608a 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -10,9 +10,9 @@ module Ci
has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline'
has_many :pipelines
- validates :cron, unless: :importing_or_inactive?, cron: true, presence: { unless: :importing_or_inactive? }
- validates :cron_timezone, cron_timezone: true, presence: { unless: :importing_or_inactive? }
- validates :ref, presence: { unless: :importing_or_inactive? }
+ validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? }
+ validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
+ validates :ref, presence: { unless: :importing? }
validates :description, presence: true
before_save :set_next_run_at
@@ -32,10 +32,6 @@ module Ci
update_attribute(:active, false)
end
- def importing_or_inactive?
- importing? || inactive?
- end
-
def runnable_by_owner?
Ability.allowed?(owner, :create_pipeline, project)
end
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
index 6359f7596b1..f734952fa6c 100644
--- a/app/models/concerns/note_on_diff.rb
+++ b/app/models/concerns/note_on_diff.rb
@@ -33,14 +33,4 @@ module NoteOnDiff
def created_at_diff?(diff_refs)
false
end
-
- private
-
- def noteable_diff_refs
- if noteable.respond_to?(:diff_sha_refs)
- noteable.diff_sha_refs
- else
- noteable.diff_refs
- end
- end
end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index c4463abdfe6..63d02b76f6b 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -84,89 +84,6 @@ module Routable
joins(:route).where(wheres.join(' OR '))
end
end
-
- # Builds a relation to find multiple objects that are nested under user membership
- #
- # Usage:
- #
- # Klass.member_descendants(1)
- #
- # Returns an ActiveRecord::Relation.
- def member_descendants(user_id)
- joins(:route).
- joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%')
- INNER JOIN members ON members.source_id = r2.source_id
- AND members.source_type = r2.source_type").
- where('members.user_id = ?', user_id)
- end
-
- # Builds a relation to find multiple objects that are nested under user
- # membership. Includes the parent, as opposed to `#member_descendants`
- # which only includes the descendants.
- #
- # Usage:
- #
- # Klass.member_self_and_descendants(1)
- #
- # Returns an ActiveRecord::Relation.
- def member_self_and_descendants(user_id)
- joins(:route).
- joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%')
- OR routes.path = r2.path
- INNER JOIN members ON members.source_id = r2.source_id
- AND members.source_type = r2.source_type").
- where('members.user_id = ?', user_id)
- end
-
- # Returns all objects in a hierarchy, where any node in the hierarchy is
- # under the user membership.
- #
- # Usage:
- #
- # Klass.member_hierarchy(1)
- #
- # Examples:
- #
- # Given the following group tree...
- #
- # _______group_1_______
- # | |
- # | |
- # nested_group_1 nested_group_2
- # | |
- # | |
- # nested_group_1_1 nested_group_2_1
- #
- #
- # ... the following results are returned:
- #
- # * the user is a member of group 1
- # => 'group_1',
- # 'nested_group_1', nested_group_1_1',
- # 'nested_group_2', 'nested_group_2_1'
- #
- # * the user is a member of nested_group_2
- # => 'group1',
- # 'nested_group_2', 'nested_group_2_1'
- #
- # * the user is a member of nested_group_2_1
- # => 'group1',
- # 'nested_group_2', 'nested_group_2_1'
- #
- # Returns an ActiveRecord::Relation.
- def member_hierarchy(user_id)
- paths = member_self_and_descendants(user_id).pluck('routes.path')
-
- return none if paths.empty?
-
- wheres = paths.map do |path|
- "#{connection.quote(path)} = routes.path
- OR
- #{connection.quote(path)} LIKE CONCAT(routes.path, '/%')"
- end
-
- joins(:route).where(wheres.join(' OR '))
- end
end
def full_name
diff --git a/app/models/concerns/select_for_project_authorization.rb b/app/models/concerns/select_for_project_authorization.rb
index 50a1d7fc3e1..58194b0ea13 100644
--- a/app/models/concerns/select_for_project_authorization.rb
+++ b/app/models/concerns/select_for_project_authorization.rb
@@ -3,7 +3,11 @@ module SelectForProjectAuthorization
module ClassMethods
def select_for_project_authorization
- select("members.user_id, projects.id AS project_id, members.access_level")
+ select("projects.id AS project_id, members.access_level")
+ end
+
+ def select_as_master_for_project_authorization
+ select(["projects.id AS project_id", "#{Gitlab::Access::MASTER} AS access_level"])
end
end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 1764004078e..2a4cff37566 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -63,7 +63,7 @@ class DiffNote < Note
return false unless supported?
return true if for_commit?
- diff_refs ||= noteable_diff_refs
+ diff_refs ||= noteable.diff_refs
self.position.diff_refs == diff_refs
end
@@ -99,7 +99,7 @@ class DiffNote < Note
self.project,
nil,
old_diff_refs: self.position.diff_refs,
- new_diff_refs: noteable_diff_refs,
+ new_diff_refs: noteable.diff_refs,
paths: self.position.paths
).execute(self)
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 6aab477f431..be944da5a67 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -38,6 +38,10 @@ class Group < Namespace
after_save :update_two_factor_requirement
class << self
+ def supports_nested_groups?
+ Gitlab::Database.postgresql?
+ end
+
# Searches for groups matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
@@ -78,7 +82,7 @@ class Group < Namespace
if current_scope.joins_values.include?(:shared_projects)
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
.where('project_namespace.share_with_group_lock = ?', false)
- .select("members.user_id, projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
+ .select("projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
else
super
end
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index eef24052a06..40e43c27f91 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -2,6 +2,6 @@ class ServiceHook < WebHook
belongs_to :service
def execute(data)
- super(data, 'service_hook')
+ WebHookService.new(self, data, 'service_hook').execute
end
end
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index c645805c6da..1584235ab00 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -3,8 +3,4 @@ class SystemHook < WebHook
default_value_for :push_events, false
default_value_for :repository_update_events, true
-
- def async_execute(data, hook_name)
- Sidekiq::Client.enqueue(SystemHookWorker, id, data, hook_name)
- end
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index a165fdc312f..7503f3739c3 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -1,6 +1,5 @@
class WebHook < ActiveRecord::Base
include Sortable
- include HTTParty
default_value_for :push_events, true
default_value_for :issues_events, false
@@ -13,52 +12,18 @@ class WebHook < ActiveRecord::Base
default_value_for :repository_update_events, false
default_value_for :enable_ssl_verification, true
+ has_many :web_hook_logs, dependent: :destroy
+
scope :push_hooks, -> { where(push_events: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true) }
- # HTTParty timeout
- default_timeout Gitlab.config.gitlab.webhook_timeout
-
validates :url, presence: true, url: true
def execute(data, hook_name)
- parsed_url = URI.parse(url)
- if parsed_url.userinfo.blank?
- response = WebHook.post(url,
- body: data.to_json,
- headers: build_headers(hook_name),
- verify: enable_ssl_verification)
- else
- post_url = url.gsub("#{parsed_url.userinfo}@", '')
- auth = {
- username: CGI.unescape(parsed_url.user),
- password: CGI.unescape(parsed_url.password)
- }
- response = WebHook.post(post_url,
- body: data.to_json,
- headers: build_headers(hook_name),
- verify: enable_ssl_verification,
- basic_auth: auth)
- end
-
- [response.code, response.to_s]
- rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
- logger.error("WebHook Error => #{e}")
- [false, e.to_s]
+ WebHookService.new(self, data, hook_name).execute
end
def async_execute(data, hook_name)
- Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data, hook_name)
- end
-
- private
-
- def build_headers(hook_name)
- headers = {
- 'Content-Type' => 'application/json',
- 'X-Gitlab-Event' => hook_name.singularize.titleize
- }
- headers['X-Gitlab-Token'] = token if token.present?
- headers
+ WebHookService.new(self, data, hook_name).async_execute
end
end
diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb
new file mode 100644
index 00000000000..2738b229d84
--- /dev/null
+++ b/app/models/hooks/web_hook_log.rb
@@ -0,0 +1,13 @@
+class WebHookLog < ActiveRecord::Base
+ belongs_to :web_hook
+
+ serialize :request_headers, Hash
+ serialize :request_data, Hash
+ serialize :response_headers, Hash
+
+ validates :web_hook, presence: true
+
+ def success?
+ response_status =~ /^2/
+ end
+end
diff --git a/app/models/label.rb b/app/models/label.rb
index ddddb6bdf8f..074239702f8 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -133,6 +133,10 @@ class Label < ActiveRecord::Base
template
end
+ def color
+ super || DEFAULT_COLOR
+ end
+
def text_color
LabelsHelper.text_color_for_bg(self.color)
end
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index d7c627432d2..ebf8fb92ab5 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -61,7 +61,7 @@ class LegacyDiffNote < Note
return true if for_commit?
return true unless diff_line
return false unless noteable
- return false if diff_refs && diff_refs != noteable_diff_refs
+ return false if diff_refs && diff_refs != noteable.diff_refs
noteable_diff = find_noteable_diff
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 2eec013fa9d..356af776b8d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -245,19 +245,6 @@ class MergeRequest < ActiveRecord::Base
end
end
- # MRs created before 8.4 don't store a MergeRequestDiff#base_commit_sha,
- # but we need to get a commit for the "View file @ ..." link by deleted files,
- # so we find the likely one if we can't get the actual one.
- # This will not be the actual base commit if the target branch was merged into
- # the source branch after the merge request was created, but it is good enough
- # for the specific purpose of linking to a commit.
- # It is not good enough for use in `Gitlab::Git::DiffRefs`, which needs the
- # true base commit, so we can't simply have `#diff_base_commit` fall back on
- # this method.
- def likely_diff_base_commit
- first_commit.try(:parent) || first_commit
- end
-
def diff_start_commit
if persisted?
merge_request_diff.start_commit
@@ -322,21 +309,14 @@ class MergeRequest < ActiveRecord::Base
end
def diff_refs
- return unless diff_start_commit || diff_base_commit
-
- Gitlab::Diff::DiffRefs.new(
- base_sha: diff_base_sha,
- start_sha: diff_start_sha,
- head_sha: diff_head_sha
- )
- end
-
- # Return diff_refs instance trying to not touch the git repository
- def diff_sha_refs
- if merge_request_diff && merge_request_diff.diff_refs_by_sha?
+ if persisted?
merge_request_diff.diff_refs
else
- diff_refs
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: diff_base_sha,
+ start_sha: diff_start_sha,
+ head_sha: diff_head_sha
+ )
end
end
@@ -870,7 +850,7 @@ class MergeRequest < ActiveRecord::Base
end
def has_complete_diff_refs?
- diff_sha_refs && diff_sha_refs.complete?
+ diff_refs && diff_refs.complete?
end
def update_diff_notes_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 6e3917a10a3..1bd61c1d465 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -150,6 +150,29 @@ class MergeRequestDiff < ActiveRecord::Base
)
end
+ # MRs created before 8.4 don't store their true diff refs (start and base),
+ # but we need to get a commit SHA for the "View file @ ..." link by a file,
+ # so we use an approximation of the diff refs if we can't get the actual one.
+ #
+ # These will not be the actual diff refs if the target branch was merged into
+ # the source branch after the merge request was created, but it is good enough
+ # for the specific purpose of linking to a commit.
+ #
+ # It is not good enough for highlighting diffs, so we can't simply pass
+ # these as `diff_refs.`
+ def fallback_diff_refs
+ real_refs = diff_refs
+ return real_refs if real_refs
+
+ likely_base_commit_sha = (first_commit&.parent || first_commit)&.sha
+
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: likely_base_commit_sha,
+ start_sha: safe_start_commit_sha,
+ head_sha: head_commit_sha
+ )
+ end
+
def diff_refs_by_sha?
base_commit_sha? && head_commit_sha? && start_commit_sha?
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index c06bfe0ccdd..b04bed4c014 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -107,7 +107,7 @@ class Milestone < ActiveRecord::Base
end
def participants
- User.joins(assigned_issues: :milestone).where("milestones.id = ?", id)
+ User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq
end
def self.sort(method)
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 4d59267f71d..aebee06d560 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -176,26 +176,20 @@ class Namespace < ActiveRecord::Base
projects.with_shared_runners.any?
end
- # Scopes the model on ancestors of the record
+ # Returns all the ancestors of the current namespaces.
def ancestors
- if parent_id
- path = route ? route.path : full_path
- paths = []
+ return self.class.none unless parent_id
- until path.blank?
- path = path.rpartition('/').first
- paths << path
- end
-
- self.class.joins(:route).where('routes.path IN (?)', paths).reorder('routes.path ASC')
- else
- self.class.none
- end
+ Gitlab::GroupHierarchy.
+ new(self.class.where(id: parent_id)).
+ base_and_ancestors
end
- # Scopes the model on direct and indirect children of the record
+ # Returns all the descendants of the current namespace.
def descendants
- self.class.joins(:route).merge(Route.inside_path(route.path)).reorder('routes.path ASC')
+ Gitlab::GroupHierarchy.
+ new(self.class.where(parent_id: id)).
+ base_and_descendants
end
def user_ids_for_project_authorizations
diff --git a/app/models/project.rb b/app/models/project.rb
index cfca0dcd2f2..29af57d7664 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -205,8 +205,8 @@ class Project < ActiveRecord::Base
presence: true,
dynamic_path: true,
length: { maximum: 255 },
- format: { with: Gitlab::Regex.project_path_format_regex,
- message: Gitlab::Regex.project_path_regex_message },
+ format: { with: Gitlab::PathRegex.project_path_format_regex,
+ message: Gitlab::PathRegex.project_path_format_message },
uniqueness: { scope: :namespace_id }
validates :namespace, presence: true
@@ -380,11 +380,9 @@ class Project < ActiveRecord::Base
end
def reference_pattern
- name_pattern = Gitlab::Regex::FULL_NAMESPACE_REGEX_STR
-
%r{
- ((?<namespace>#{name_pattern})\/)?
- (?<project>#{name_pattern})
+ ((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
+ (?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
}x
end
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index 4c7f4f5a429..def09675253 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -6,6 +6,12 @@ class ProjectAuthorization < ActiveRecord::Base
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true
+ def self.select_from_union(union)
+ select(['project_id', 'MAX(access_level) AS access_level']).
+ from("(#{union.to_sql}) #{ProjectAuthorization.table_name}").
+ group(:project_id)
+ end
+
def self.insert_authorizations(rows, per_batch = 1000)
rows.each_slice(per_batch) do |slice|
tuples = slice.map do |tuple|
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index a91a986e195..fe869623833 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -2,9 +2,10 @@ class JiraService < IssueTrackerService
include Gitlab::Routing.url_helpers
validates :url, url: true, presence: true, if: :activated?
+ validates :api_url, url: true, allow_blank: true
validates :project_key, presence: true, if: :activated?
- prop_accessor :username, :password, :url, :project_key,
+ prop_accessor :username, :password, :url, :api_url, :project_key,
:jira_issue_transition_id, :title, :description
before_update :reset_password
@@ -25,20 +26,18 @@ class JiraService < IssueTrackerService
super do
self.properties = {
title: issues_tracker['title'],
- url: issues_tracker['url']
+ url: issues_tracker['url'],
+ api_url: issues_tracker['api_url']
}
end
end
def reset_password
- # don't reset the password if a new one is provided
- if url_changed? && !password_touched?
- self.password = nil
- end
+ self.password = nil if reset_password?
end
def options
- url = URI.parse(self.url)
+ url = URI.parse(client_url)
{
username: self.username,
@@ -87,7 +86,8 @@ class JiraService < IssueTrackerService
def fields
[
- { type: 'text', name: 'url', title: 'URL', placeholder: 'https://jira.example.com' },
+ { type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com' },
+ { type: 'text', name: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' },
{ type: 'text', name: 'project_key', placeholder: 'Project Key' },
{ type: 'text', name: 'username', placeholder: '' },
{ type: 'password', name: 'password', placeholder: '' },
@@ -186,7 +186,7 @@ class JiraService < IssueTrackerService
end
def test_settings
- return unless url.present?
+ return unless client_url.present?
# Test settings by getting the project
jira_request { jira_project.present? }
end
@@ -236,13 +236,13 @@ class JiraService < IssueTrackerService
end
def send_message(issue, message, remote_link_props)
- return unless url.present?
+ return unless client_url.present?
jira_request do
if issue.comments.build.save!(body: message)
remote_link = issue.remotelink.build
remote_link.save!(remote_link_props)
- result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
+ result_message = "#{self.class.name} SUCCESS: Successfully posted to #{client_url}."
end
Rails.logger.info(result_message)
@@ -295,7 +295,20 @@ 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: #{url} - #{e.message}"
+ Rails.logger.info "#{self.class.name} Send message ERROR: #{client_url} - #{e.message}"
nil
end
+
+ def client_url
+ api_url.present? ? api_url : url
+ end
+
+ def reset_password?
+ # don't reset the password if a new one is provided
+ return false if password_touched?
+ return true if api_url_changed?
+ return false if api_url.present?
+
+ url_changed?
+ end
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index b2494a0be6e..8977a7cdafe 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -77,6 +77,14 @@ class KubernetesService < DeploymentService
]
end
+ def actual_namespace
+ if namespace.present?
+ namespace
+ else
+ default_namespace
+ end
+ end
+
# Check we can connect to the Kubernetes API
def test(*args)
kubeclient = build_kubeclient!
@@ -91,7 +99,7 @@ class KubernetesService < DeploymentService
variables = [
{ key: 'KUBE_URL', value: api_url, public: true },
{ key: 'KUBE_TOKEN', value: token, public: false },
- { key: 'KUBE_NAMESPACE', value: namespace_variable, public: true }
+ { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true }
]
if ca_pem.present?
@@ -110,7 +118,7 @@ class KubernetesService < DeploymentService
with_reactive_cache do |data|
pods = data.fetch(:pods, nil)
filter_pods(pods, app: environment.slug).
- flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }.
+ flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }.
each { |terminal| add_terminal_auth(terminal, terminal_auth) }
end
end
@@ -124,7 +132,7 @@ class KubernetesService < DeploymentService
# Store as hashes, rather than as third-party types
pods = begin
- kubeclient.get_pods(namespace: namespace).as_json
+ kubeclient.get_pods(namespace: actual_namespace).as_json
rescue KubeException => err
raise err unless err.error_code == 404
[]
@@ -142,20 +150,12 @@ class KubernetesService < DeploymentService
default_namespace || TEMPLATE_PLACEHOLDER
end
- def namespace_variable
- if namespace.present?
- namespace
- else
- default_namespace
- end
- end
-
def default_namespace
"#{project.path}-#{project.id}" if project.present?
end
def build_kubeclient!(api_path: 'api', api_version: 'v1')
- raise "Incomplete settings" unless api_url && namespace && token
+ raise "Incomplete settings" unless api_url && actual_namespace && token
::Kubeclient::Client.new(
join_api_url(api_path),
diff --git a/app/models/user.rb b/app/models/user.rb
index cf3914568a6..3f816a250c2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -10,9 +10,12 @@ class User < ActiveRecord::Base
include Sortable
include CaseSensitivity
include TokenAuthenticatable
+ include IgnorableColumn
DEFAULT_NOTIFICATION_LEVEL = :participating
+ ignore_column :authorized_projects_populated
+
add_authentication_token_field :authentication_token
add_authentication_token_field :incoming_email_token
add_authentication_token_field :rss_token
@@ -218,7 +221,6 @@ class User < ActiveRecord::Base
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active).non_internal }
- scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
@@ -368,7 +370,7 @@ class User < ActiveRecord::Base
def reference_pattern
%r{
#{Regexp.escape(reference_prefix)}
- (?<user>#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR})
+ (?<user>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})
}x
end
@@ -510,23 +512,16 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})")
end
- def nested_groups
- Group.member_descendants(id)
- end
-
+ # Returns a relation of groups the user has access to, including their parent
+ # and child groups (recursively).
def all_expanded_groups
- Group.member_hierarchy(id)
+ Gitlab::GroupHierarchy.new(groups).all_groups
end
def expanded_groups_requiring_two_factor_authentication
all_expanded_groups.where(require_two_factor_authentication: true)
end
- def nested_groups_projects
- Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL').
- member_descendants(id)
- end
-
def refresh_authorized_projects
Users::RefreshAuthorizedProjectsService.new(self).execute
end
@@ -535,18 +530,15 @@ class User < ActiveRecord::Base
project_authorizations.where(project_id: project_ids).delete_all
end
- def set_authorized_projects_column
- unless authorized_projects_populated
- update_column(:authorized_projects_populated, true)
- end
- end
-
def authorized_projects(min_access_level = nil)
- refresh_authorized_projects unless authorized_projects_populated
-
- # We're overriding an association, so explicitly call super with no arguments or it would be passed as `force_reload` to the association
+ # We're overriding an association, so explicitly call super with no
+ # arguments or it would be passed as `force_reload` to the association
projects = super()
- projects = projects.where('project_authorizations.access_level >= ?', min_access_level) if min_access_level
+
+ if min_access_level
+ projects = projects.
+ where('project_authorizations.access_level >= ?', min_access_level)
+ end
projects
end
@@ -919,13 +911,13 @@ class User < ActiveRecord::Base
end
def assigned_open_merge_requests_count(force: false)
- Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force) do
+ Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end
end
def assigned_open_issues_count(force: false)
- Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do
+ Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end
end
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index b3247ae36dd..f7eb75395b5 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -39,6 +39,7 @@ class MergeRequestEntity < IssuableEntity
expose :commits_count
expose :cannot_be_merged?, as: :has_conflicts
expose :can_be_merged?, as: :can_be_merged
+ expose :remove_source_branch?, as: :remove_source_branch
expose :project_archived do |merge_request|
merge_request.project.archived?
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index f1030912c68..85c616ca576 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -28,6 +28,7 @@ module Issues
notification_service.close_issue(issue, current_user) if notifications
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
+ invalidate_cache_counts(issue.assignees, issue)
end
issue
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index 40fbe354492..80ea6312768 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -8,6 +8,7 @@ module Issues
create_note(issue)
notification_service.reopen_issue(issue, current_user)
execute_hooks(issue, 'reopen')
+ invalidate_cache_counts(issue.assignees, issue)
end
issue
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index f2053bda83a..2ffc989ed71 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -13,6 +13,7 @@ module MergeRequests
notification_service.close_mr(merge_request, current_user)
todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close')
+ invalidate_cache_counts(merge_request.assignees, merge_request)
end
merge_request
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index e8fb1b59752..f0d998731d7 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -13,6 +13,7 @@ module MergeRequests
create_note(merge_request)
notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge')
+ invalidate_cache_counts(merge_request.assignees, merge_request)
end
private
diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb
index 54b19e6d651..f2fddf7f345 100644
--- a/app/services/merge_requests/reopen_service.rb
+++ b/app/services/merge_requests/reopen_service.rb
@@ -10,6 +10,7 @@ module MergeRequests
execute_hooks(merge_request, 'reopen')
merge_request.reload_diff(current_user)
merge_request.mark_as_unchecked
+ invalidate_cache_counts(merge_request.assignees, merge_request)
end
merge_request
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 22736c71725..1d4d03a8b7d 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -12,7 +12,7 @@ class SearchService
@project =
if params[:project_id].present?
the_project = Project.find_by(id: params[:project_id])
- can?(current_user, :download_code, the_project) ? the_project : nil
+ can?(current_user, :read_project, the_project) ? the_project : nil
else
nil
end
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index 8f6f5b937c4..3e07b811027 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -73,12 +73,11 @@ module Users
# remove - The IDs of the authorization rows to remove.
# add - Rows to insert in the form `[user id, project id, access level]`
def update_authorizations(remove = [], add = [])
- return if remove.empty? && add.empty? && user.authorized_projects_populated
+ return if remove.empty? && add.empty?
User.transaction do
user.remove_project_authorizations(remove) unless remove.empty?
ProjectAuthorization.insert_authorizations(add) unless add.empty?
- user.set_authorized_projects_column
end
# Since we batch insert authorization rows, Rails' associations may get
@@ -101,38 +100,13 @@ module Users
end
def fresh_authorizations
- ProjectAuthorization.
- unscoped.
- select('project_id, MAX(access_level) AS access_level').
- from("(#{project_authorizations_union.to_sql}) #{ProjectAuthorization.table_name}").
- group(:project_id)
- end
-
- private
-
- # Returns a union query of projects that the user is authorized to access
- def project_authorizations_union
- relations = [
- # Personal projects
- user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
-
- # Projects the user is a member of
- user.projects.select_for_project_authorization,
-
- # Projects of groups the user is a member of
- user.groups_projects.select_for_project_authorization,
-
- # Projects of subgroups of groups the user is a member of
- user.nested_groups_projects.select_for_project_authorization,
-
- # Projects shared with groups the user is a member of
- user.groups.joins(:shared_projects).select_for_project_authorization,
-
- # Projects shared with subgroups of groups the user is a member of
- user.nested_groups.joins(:shared_projects).select_for_project_authorization
- ]
+ klass = if Group.supports_nested_groups?
+ Gitlab::ProjectAuthorizations::WithNestedGroups
+ else
+ Gitlab::ProjectAuthorizations::WithoutNestedGroups
+ end
- Gitlab::SQL::Union.new(relations)
+ klass.new(user).calculate
end
end
end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
new file mode 100644
index 00000000000..4241b912d5b
--- /dev/null
+++ b/app/services/web_hook_service.rb
@@ -0,0 +1,120 @@
+class WebHookService
+ class InternalErrorResponse
+ attr_reader :body, :headers, :code
+
+ def initialize
+ @headers = HTTParty::Response::Headers.new({})
+ @body = ''
+ @code = 'internal error'
+ end
+ end
+
+ include HTTParty
+
+ # HTTParty timeout
+ default_timeout Gitlab.config.gitlab.webhook_timeout
+
+ attr_accessor :hook, :data, :hook_name
+
+ def initialize(hook, data, hook_name)
+ @hook = hook
+ @data = data
+ @hook_name = hook_name
+ end
+
+ def execute
+ start_time = Time.now
+
+ response = if parsed_url.userinfo.blank?
+ make_request(hook.url)
+ else
+ make_request_with_auth
+ end
+
+ log_execution(
+ trigger: hook_name,
+ url: hook.url,
+ request_data: data,
+ response: response,
+ execution_duration: Time.now - start_time
+ )
+
+ [response.code, response.to_s]
+ rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
+ log_execution(
+ trigger: hook_name,
+ url: hook.url,
+ request_data: data,
+ response: InternalErrorResponse.new,
+ execution_duration: Time.now - start_time,
+ error_message: e.to_s
+ )
+
+ Rails.logger.error("WebHook Error => #{e}")
+
+ [nil, e.to_s]
+ end
+
+ def async_execute
+ Sidekiq::Client.enqueue(WebHookWorker, hook.id, data, hook_name)
+ end
+
+ private
+
+ def parsed_url
+ @parsed_url ||= URI.parse(hook.url)
+ end
+
+ def make_request(url, basic_auth = false)
+ self.class.post(url,
+ body: data.to_json,
+ headers: build_headers(hook_name),
+ verify: hook.enable_ssl_verification,
+ basic_auth: basic_auth)
+ end
+
+ def make_request_with_auth
+ post_url = hook.url.gsub("#{parsed_url.userinfo}@", '')
+ basic_auth = {
+ username: CGI.unescape(parsed_url.user),
+ password: CGI.unescape(parsed_url.password)
+ }
+ make_request(post_url, basic_auth)
+ end
+
+ def log_execution(trigger:, url:, request_data:, response:, execution_duration:, error_message: nil)
+ # logging for ServiceHook's is not available
+ return if hook.is_a?(ServiceHook)
+
+ WebHookLog.create(
+ web_hook: hook,
+ trigger: trigger,
+ url: url,
+ execution_duration: execution_duration,
+ request_headers: build_headers(hook_name),
+ request_data: request_data,
+ response_headers: format_response_headers(response),
+ response_body: response.body,
+ response_status: response.code,
+ internal_error_message: error_message
+ )
+ end
+
+ def build_headers(hook_name)
+ @headers ||= begin
+ {
+ 'Content-Type' => 'application/json',
+ 'X-Gitlab-Event' => hook_name.singularize.titleize
+ }.tap do |hash|
+ hash['X-Gitlab-Token'] = hook.token if hook.token.present?
+ end
+ end
+ end
+
+ # Make response headers more stylish
+ # Net::HTTPHeader has downcased hash with arrays: { 'content-type' => ['text/html; charset=utf-8'] }
+ # This method format response to capitalized hash with strings: { 'Content-Type' => 'text/html; charset=utf-8' }
+ def format_response_headers(response)
+ response.headers.each_capitalized.to_h
+ end
+end
diff --git a/app/validators/dynamic_path_validator.rb b/app/validators/dynamic_path_validator.rb
index 8d4d7180baf..6819886ebf4 100644
--- a/app/validators/dynamic_path_validator.rb
+++ b/app/validators/dynamic_path_validator.rb
@@ -3,16 +3,20 @@
# Custom validator for GitLab path values.
# These paths are assigned to `Namespace` (& `Group` as a subclass) & `Project`
#
-# Values are checked for formatting and exclusion from a list of reserved path
+# Values are checked for formatting and exclusion from a list of illegal path
# names.
class DynamicPathValidator < ActiveModel::EachValidator
class << self
- def valid_namespace_path?(path)
- "#{path}/" =~ Gitlab::Regex.full_namespace_path_regex
+ def valid_user_path?(path)
+ "#{path}/" =~ Gitlab::PathRegex.root_namespace_path_regex
+ end
+
+ def valid_group_path?(path)
+ "#{path}/" =~ Gitlab::PathRegex.full_namespace_path_regex
end
def valid_project_path?(path)
- "#{path}/" =~ Gitlab::Regex.full_project_path_regex
+ "#{path}/" =~ Gitlab::PathRegex.full_project_path_regex
end
end
@@ -24,14 +28,16 @@ class DynamicPathValidator < ActiveModel::EachValidator
case record
when Project
self.class.valid_project_path?(full_path)
- else
- self.class.valid_namespace_path?(full_path)
+ when Group
+ self.class.valid_group_path?(full_path)
+ else # User or non-Group Namespace
+ self.class.valid_user_path?(full_path)
end
end
def validate_each(record, attribute, value)
- unless value =~ Gitlab::Regex.namespace_regex
- record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
+ unless value =~ Gitlab::PathRegex.namespace_format_regex
+ record.errors.add(attribute, Gitlab::PathRegex.namespace_format_message)
return
end
diff --git a/app/views/admin/hook_logs/_index.html.haml b/app/views/admin/hook_logs/_index.html.haml
new file mode 100644
index 00000000000..7dd9943190f
--- /dev/null
+++ b/app/views/admin/hook_logs/_index.html.haml
@@ -0,0 +1,37 @@
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Recent Deliveries
+ %p When an event in GitLab triggers a webhook, you can use the request details to figure out if something went wrong.
+ .col-lg-9
+ - if hook_logs.any?
+ %table.table
+ %thead
+ %tr
+ %th Status
+ %th Trigger
+ %th URL
+ %th Elapsed time
+ %th Request time
+ %th
+ - hook_logs.each do |hook_log|
+ %tr
+ %td
+ = render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
+ %td.hidden-xs
+ %span.label.label-gray.deploy-project-label
+ = hook_log.trigger.singularize.titleize
+ %td
+ = truncate(hook_log.url, length: 50)
+ %td.light
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} ms
+ %td.light
+ = time_ago_with_tooltip(hook_log.created_at)
+ %td
+ = link_to 'View details', admin_hook_hook_log_path(hook, hook_log)
+
+ = paginate hook_logs, theme: 'gitlab'
+
+ - else
+ .settings-message.text-center
+ You don't have any webhooks deliveries
diff --git a/app/views/admin/hook_logs/show.html.haml b/app/views/admin/hook_logs/show.html.haml
new file mode 100644
index 00000000000..56127bacda2
--- /dev/null
+++ b/app/views/admin/hook_logs/show.html.haml
@@ -0,0 +1,10 @@
+- page_title 'Request details'
+%h3.page-title
+ Request details
+
+%hr
+
+= link_to 'Resend Request', retry_admin_hook_hook_log_path(@hook, @hook_log), class: "btn btn-default pull-right prepend-left-10"
+
+= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
+
diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml
index 0777f5e2629..0e35a1905bf 100644
--- a/app/views/admin/hooks/edit.html.haml
+++ b/app/views/admin/hooks/edit.html.haml
@@ -12,3 +12,9 @@
= render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
= f.submit 'Save changes', class: 'btn btn-create'
+ = link_to 'Test hook', test_admin_hook_path(@hook), class: 'btn btn-default'
+ = link_to 'Remove', admin_hook_path(@hook), method: :delete, class: 'btn btn-remove pull-right', data: { confirm: 'Are you sure?' }
+
+%hr
+
+= render partial: 'admin/hook_logs/index', locals: { hook: @hook, hook_logs: @hook_logs }
diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml
index 2e5f120c4e4..9b9559c7fe5 100644
--- a/app/views/admin/system_info/show.html.haml
+++ b/app/views/admin/system_info/show.html.haml
@@ -31,3 +31,8 @@
%h1 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}
%p= disk[:disk_name]
%p= disk[:mount_path]
+ .col-sm-4
+ .light-well
+ %h4 Uptime
+ .data
+ %h1= time_ago_with_tooltip(Rails.application.config.booted_at)
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index e1b270a08c2..a676eba2aee 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -1,6 +1,3 @@
-.hidden-xs
- = render "events/event_last_push", event: @last_push
-
.nav-block.activities
.controls
= link_to dashboard_projects_path(rss_url_options), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do
diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml
index 190ad4b40a5..f893c3e1675 100644
--- a/app/views/dashboard/activity.html.haml
+++ b/app/views/dashboard/activity.html.haml
@@ -1,10 +1,16 @@
+- @no_container = true
+
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
- page_title "Activity"
- header_title "Activity", activity_dashboard_path
-= render 'dashboard/activity_head'
+.hidden-xs
+ = render "projects/last_push"
+
+%div{ class: container_class }
+ = render 'dashboard/activity_head'
-%section.activities
- = render 'activities'
+ %section.activities
+ = render 'activities'
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 596499230f9..3b2a555a143 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -1,19 +1,21 @@
+- @no_container = true
+
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
- page_title "Projects"
- header_title "Projects", dashboard_projects_path
-- unless show_user_callout?
- = render 'shared/user_callout'
+= render "projects/last_push"
-- if @projects.any? || params[:name]
- = render 'dashboard/projects_head'
+%div{ class: container_class }
+ - unless show_user_callout?
+ = render 'shared/user_callout'
-- if @last_push
- = render "events/event_last_push", event: @last_push
+ - if @projects.any? || params[:name]
+ = render 'dashboard/projects_head'
-- if @projects.any? || params[:name]
- = render 'projects'
-- else
- = render "zero_authorized_projects"
+ - if @projects.any? || params[:name]
+ = render 'projects'
+ - else
+ = render "zero_authorized_projects"
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index 162ae153b1c..99efe9c9b86 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -1,13 +1,15 @@
+- @no_container = true
+
- page_title "Starred Projects"
- header_title "Projects", dashboard_projects_path
-= render 'dashboard/projects_head'
+= render "projects/last_push"
-- if @last_push
- = render "events/event_last_push", event: @last_push
+%div{ class: container_class }
+ = render 'dashboard/projects_head'
-- if @projects.any? || params[:filter_projects]
- = render 'projects'
-- else
- %h3 You don't have starred projects yet
- %p.slead Visit project page and press on star icon and it will appear on this page.
+ - if @projects.any? || params[:filter_projects]
+ = render 'projects'
+ - else
+ %h3 You don't have starred projects yet
+ %p.slead Visit project page and press on star icon and it will appear on this page.
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index a2f6a7ab1cb..d696577278d 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -8,7 +8,7 @@
= f.text_field :name, class: "form-control top", required: true, title: "This field is required."
.username.form-group
= f.label :username
- = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, required: true, title: 'Please create a username with only alphanumeric characters.'
+ = f.text_field :username, class: "form-control middle", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: 'Please create a username with only alphanumeric characters.'
%p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability...
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index c3f55ff821f..70042dee20f 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -3,7 +3,7 @@
.diff-file.file-holder
.js-file-title.file-title
- = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_path(discussion), show_toggle: false
+ = render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false
.diff-content.code.js-syntax-highlight
%table
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index 7ba3f3f6c42..db5ab939948 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -1,10 +1,11 @@
.discussion-notes
%ul.notes{ data: { discussion_id: discussion.id } }
= render partial: "shared/notes/note", collection: discussion.notes, as: :note
- .flash-container
- - if current_user
- .discussion-reply-holder
+ .flash-container
+
+ .discussion-reply-holder
+ - if can_create_note?
- if discussion.potentially_resolvable?
- line_type = local_assigns.fetch(:line_type, nil)
@@ -19,3 +20,10 @@
= render "discussions/jump_to_next", discussion: discussion
- else
= link_to_reply_discussion(discussion)
+ - elsif !current_user
+ .disabled-comment.text-center
+ Please
+ = link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
+ or
+ = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes')
+ to reply
diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml
deleted file mode 100644
index 1584695a62b..00000000000
--- a/app/views/events/_event_last_push.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-- if show_last_push_widget?(event)
- .row-content-block.clear-block.last-push-widget
- .event-last-push
- .event-last-push-text
- %span You pushed to
- = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name), title: h(event.project.name) do
- %strong= event.ref_name
- %span at
- %strong= link_to_project event.project
- #{time_ago_with_tooltip(event.created_at)}
-
- .pull-right
- = link_to new_mr_path_from_push_event(event), title: "New merge request", class: "btn btn-info btn-sm" do
- Create merge request
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index c0943100ae3..769ac655d0a 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -7,7 +7,7 @@
%span.pushed #{event.action_name} #{event.ref_type}
%strong
- commits_link = namespace_project_commits_path(project.namespace, project, event.ref_name)
- = link_to_if project.repository.branch_exists?(event.ref_name), event.ref_name, commits_link
+ = link_to_if project.repository.branch_exists?(event.ref_name), event.ref_name, commits_link, class: 'ref-name'
= render "events/event_scope", event: event
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index d7851c79990..fd6e7111f38 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -1,6 +1,3 @@
-.hidden-xs
- = render "events/event_last_push", event: @last_push
-
.nav-block
.controls
= link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do
diff --git a/app/views/groups/_head.html.haml b/app/views/groups/_head.html.haml
index 873504099d4..0f63774fb9b 100644
--- a/app/views/groups/_head.html.haml
+++ b/app/views/groups/_head.html.haml
@@ -12,3 +12,6 @@
= link_to activity_group_path(@group), title: 'Activity' do
%span
Activity
+
+.hidden-xs
+ = render "projects/last_push"
diff --git a/app/views/groups/_show_nav.html.haml b/app/views/groups/_show_nav.html.haml
index b2097e88741..35b75bc0923 100644
--- a/app/views/groups/_show_nav.html.haml
+++ b/app/views/groups/_show_nav.html.haml
@@ -2,6 +2,7 @@
= nav_link(page: group_path(@group)) do
= link_to group_path(@group) do
Projects
- = nav_link(page: subgroups_group_path(@group)) do
- = link_to subgroups_group_path(@group) do
- Subgroups
+ - if Group.supports_nested_groups?
+ = nav_link(page: subgroups_group_path(@group)) do
+ = link_to subgroups_group_path(@group) do
+ Subgroups
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 18997baa998..80a8ba4a755 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -6,7 +6,6 @@
= render 'groups/head'
= render 'groups/home_panel'
-
.groups-header{ class: container_class }
.top-area
= render 'groups/show_nav'
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index d068c895fa3..86779eeaf15 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -5,7 +5,7 @@
.fade-right
= icon('angle-right')
%ul.nav-links.scrolling-tabs
- = nav_link(controller: %w(dashboard admin projects users groups builds runners), html_options: {class: 'home'}) do
+ = nav_link(controller: %w(dashboard admin projects users groups builds runners cohorts), html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span
Overview
@@ -17,7 +17,7 @@
= link_to admin_broadcast_messages_path, title: 'Messages' do
%span
Messages
- = nav_link(controller: :hooks) do
+ = nav_link(controller: [:hooks, :hook_logs]) do
= link_to admin_hooks_path, title: 'Hooks' do
%span
System Hooks
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index 02eb7c8462c..546376aeed8 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -27,40 +27,38 @@
%h4 #{pluralize @message.diffs_count, "changed file"}:
%ul
- - @message.diffs.each do |diff|
+ - @message.diffs.each do |diff_file|
%li.file-stats
- %a{ href: "#{@message.target_url if @message.disable_diffs?}##{hexdigest(diff.file_path)}" }
- - if diff.deleted_file
+ %a{ href: "#{@message.target_url if @message.disable_diffs?}##{hexdigest(diff_file.file_path)}" }
+ - if diff_file.deleted_file?
%span.deleted-file
&minus;
- = diff.old_path
- - elsif diff.renamed_file
- = diff.old_path
+ = diff_file.old_path
+ - elsif diff_file.renamed_file?
+ = diff_file.old_path
&rarr;
- = diff.new_path
- - elsif diff.new_file
+ = diff_file.new_path
+ - elsif diff_file.new_file?
%span.new-file
&#43;
- = diff.new_path
+ = diff_file.new_path
- else
- = diff.new_path
+ = diff_file.new_path
- unless @message.disable_diffs?
- - diff_files = @message.diffs
-
- if @message.compare_timeout
%h5 The diff was not included because it is too large.
- else
%h4 Changes:
- - diff_files.each do |diff_file|
+ - @message.diffs.each do |diff_file|
- file_hash = hexdigest(diff_file.file_path)
%li{ id: file_hash }
%a{ href: @message.target_url + "##{file_hash}" }<
- - if diff_file.deleted_file
+ - if diff_file.deleted_file?
%strong<
= diff_file.old_path
deleted
- - elsif diff_file.renamed_file
+ - elsif diff_file.renamed_file?
%strong<
= diff_file.old_path
&rarr;
diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml
index 5ac23aa3997..895d8807e47 100644
--- a/app/views/notify/repository_push_email.text.haml
+++ b/app/views/notify/repository_push_email.text.haml
@@ -15,15 +15,15 @@
\
#{pluralize @message.diffs_count, "changed file"}:
\
- - @message.diffs.each do |diff|
- - if diff.deleted_file
- \- − #{diff.old_path}
- - elsif diff.renamed_file
- \- #{diff.old_path} → #{diff.new_path}
- - elsif diff.new_file
- \- + #{diff.new_path}
+ - @message.diffs.each do |diff_file|
+ - if diff_file.deleted_file?
+ \- − #{diff_file.old_path}
+ - elsif diff_file.renamed_file?
+ \- #{diff_file.old_path} → #{diff_file.new_path}
+ - elsif diff_file.new_file?
+ \- + #{diff_file.new_path}
- else
- \- #{diff.new_path}
+ \- #{diff_file.new_path}
- unless @message.disable_diffs?
- if @message.compare_timeout
\
@@ -36,9 +36,9 @@
- @message.diffs.each do |diff_file|
\
\=====================================
- - if diff_file.deleted_file
+ - if diff_file.deleted_file?
#{diff_file.old_path} deleted
- - elsif diff_file.renamed_file
+ - elsif diff_file.renamed_file?
#{diff_file.old_path} → #{diff_file.new_path}
- else
= diff_file.new_path
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index f5bb7364d4a..10f581d751b 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,5 +1,3 @@
-- @no_container = true
-
%div{ class: container_class }
.nav-block.activity-filter-block.activities
.controls
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index f8a6e98d280..e8b1940af2d 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -1,18 +1,18 @@
-- if event = last_push_event
- - if show_last_push_widget?(event)
- .row-content-block.top-block.hidden-xs.white
- %div{ class: container_class }
- .event-last-push
- .event-last-push-text
- %span You pushed to
- = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name, class: 'commit-sha') do
- %strong= event.ref_name
- - if @project && event.project != @project
- %span at
- %strong= link_to_project event.project
- = clipboard_button(text: event.ref_name, class: 'btn-clipboard btn-transparent', title: 'Copy branch to clipboard')
- #{time_ago_with_tooltip(event.created_at)}
+- event = last_push_event
+- if event && show_last_push_widget?(event)
+ .row-content-block.top-block.hidden-xs.white
+ .event-last-push
+ .event-last-push-text
+ %span You pushed to
+ %strong
+ = link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name), class: 'ref-name'
- .pull-right
- = link_to new_mr_path_from_push_event(event), title: "New merge request", class: "btn btn-info btn-sm" do
- Create merge request
+ - if event.project != @project
+ %span at
+ %strong= link_to_project event.project
+
+ #{time_ago_with_tooltip(event.created_at)}
+
+ .pull-right
+ = link_to new_mr_path_from_push_event(event), title: "New merge request", class: "btn btn-info btn-sm" do
+ Create merge request
diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml
index 27c8e3c7fca..ef8d8051cbf 100644
--- a/app/views/projects/activity.html.haml
+++ b/app/views/projects/activity.html.haml
@@ -1,3 +1,5 @@
+- @no_container = true
+
- page_title "Activity"
= render "projects/head"
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 67f57b5e4b9..41f75a491a5 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -1,13 +1,14 @@
- @no_container = true
+
- page_title @blob.path, @ref
= render "projects/commits/head"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('blob')
-%div{ class: container_class }
- = render 'projects/last_push'
+= render 'projects/last_push'
+%div{ class: container_class }
#tree-holder.tree-holder
= render 'blob', blob: @blob
diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml
index 48f8c656080..e8db868f49b 100644
--- a/app/views/projects/boards/components/sidebar/_assignee.html.haml
+++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml
@@ -14,7 +14,10 @@
name: "issue[assignee_ids][]",
":value" => "assignee.id",
"v-if" => "issue.assignees",
- "v-for" => "assignee in issue.assignees" }
+ "v-for" => "assignee in issue.assignees",
+ ":data-avatar_url" => "assignee.avatar",
+ ":data-name" => "assignee.name",
+ ":data-username" => "assignee.username" }
.dropdown
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: "button", ref: "assigneeDropdown", data: { toggle: "dropdown", field_name: "issue[assignee_ids][]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", multi_select: "true", 'max-select' => 1, dropdown: { header: 'Assignee' } },
":data-issuable-id" => "issue.id",
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index c781e423c4d..c7e22a0b4ec 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -1,12 +1,12 @@
-.diff-content.diff-wrap-lines
- -# Skip all non non-supported blobs
- - return unless blob.respond_to?(:text?)
+- blob = diff_file.blob
+
+.diff-content
- if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is too large.
- elsif blob.too_large?
.nothing-here-block The file could not be displayed because it is too large.
- elsif blob.readable_text?
- - if !project.repository.diffable?(blob)
+ - if !diff_file.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry.
- elsif diff_file.collapsed?
- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
@@ -15,20 +15,13 @@
%a.click-to-expand
Click to expand it.
- elsif diff_file.diff_lines.length > 0
- - total_lines = 0
- - if blob.lines.any?
- - total_lines = blob.lines.last.chomp == '' ? blob.lines.size - 1 : blob.lines.size
- - if diff_view == :parallel
- = render "projects/diffs/parallel_view", diff_file: diff_file, total_lines: total_lines
- - else
- = render "projects/diffs/text_file", diff_file: diff_file, total_lines: total_lines
+ = render "projects/diffs/viewers/text", diff_file: diff_file
- else
- if diff_file.mode_changed?
.nothing-here-block File mode changed
- - elsif diff_file.renamed_file
+ - elsif diff_file.renamed_file?
.nothing-here-block File moved
- elsif blob.image?
- - old_blob = diff_file.old_blob(diff_file.old_content_commit || @base_commit)
- = render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob
+ = render "projects/diffs/viewers/image", diff_file: diff_file
- else
.nothing-here-block No preview for this file type
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 71a1b9e6c05..4768438c29e 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -23,12 +23,4 @@
= render 'projects/diffs/warning', diff_files: diffs
.files{ data: { can_create_note: can_create_note } }
- - diff_files.each_with_index do |diff_file|
- - diff_commit = commit_for_diff(diff_file)
- - blob = diff_file.blob(diff_commit)
- - next unless blob
- - blob.load_all_data!(diffs.project.repository) unless blob.too_large?
- - file_hash = hexdigest(diff_file.file_path)
-
- = render 'projects/diffs/file', file_hash: file_hash, project: diffs.project,
- diff_file: diff_file, diff_commit: diff_commit, blob: blob, environment: environment
+ = render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment }
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index f22b385fc0f..b5aea217384 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,10 +1,12 @@
- environment = local_assigns.fetch(:environment, nil)
-.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) }
+- file_hash = hexdigest(diff_file.file_path)
+.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_file.content_sha) }
.js-file-title.file-title-flex-parent
.file-header-content
- = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}"
+ = render "projects/diffs/file_header", diff_file: diff_file, url: "##{file_hash}"
- unless diff_file.submodule?
+ - blob = diff_file.blob
.file-actions.hidden-xs
- if blob.readable_text?
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
@@ -15,9 +17,9 @@
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
blob: blob, link_opts: link_opts)
- = view_file_button(diff_commit.id, diff_file.new_path, project)
- = view_on_environment_button(diff_commit.id, diff_file.new_path, environment) if environment
+ = view_file_button(diff_file.content_sha, diff_file.file_path, project)
+ = view_on_environment_button(diff_file.content_sha, diff_file.file_path, environment) if environment
= render 'projects/fork_suggestion'
- = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project
+ = render 'projects/diffs/content', diff_file: diff_file
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index 4e4fdb73ae3..73c316472e3 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -3,19 +3,20 @@
- if show_toggle
%i.fa.diff-toggle-caret.fa-fw
-- if defined?(blob) && blob && diff_file.submodule?
+- if diff_file.submodule?
+ - blob = diff_file.blob
%span
= icon('archive fw')
%strong.file-title-name
- = submodule_link(blob, diff_commit.id, project.repository)
+ = submodule_link(blob, diff_file.content_sha, diff_file.repository)
= copy_file_path_button(blob.path)
- else
= conditional_link_to url.present?, url do
= blob_icon diff_file.b_mode, diff_file.file_path
- - if diff_file.renamed_file
+ - if diff_file.renamed_file?
- old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
%strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } }
= old_path
@@ -23,12 +24,13 @@
%strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
= new_path
- else
- %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
- = diff_file.new_path
- - if diff_file.deleted_file
+ %strong.file-title-name.has-tooltip{ data: { title: diff_file.file_path, container: 'body' } }
+ = diff_file.file_path
+
+ - if diff_file.deleted_file?
deleted
- = copy_file_path_button(diff_file.new_path)
+ = copy_file_path_button(diff_file.file_path)
- if diff_file.mode_changed?
%small
diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml
deleted file mode 100644
index ca10921c5e2..00000000000
--- a/app/views/projects/diffs/_image.html.haml
+++ /dev/null
@@ -1,69 +0,0 @@
-- diff = diff_file.diff
-- file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path))
-// diff_refs will be nil for orphaned commits (e.g. first commit in repo)
-- if diff_file.old_ref
- - old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path))
-
-- if diff.renamed_file || diff.new_file || diff.deleted_file
- .image
- %span.wrap
- .frame{ class: image_diff_class(diff) }
- %img{ src: diff.deleted_file ? old_file_raw_path : file_raw_path, alt: diff.new_path }
- %p.image-info= number_to_human_size(file.size)
-- else
- .image
- .two-up.view
- %span.wrap
- .frame.deleted
- %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path)) }
- %img{ src: old_file_raw_path, alt: diff.old_path }
- %p.image-info.hide
- %span.meta-filesize= number_to_human_size(old_file.size)
- |
- %b W:
- %span.meta-width
- |
- %b H:
- %span.meta-height
- %span.wrap
- .frame.added
- %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path)) }
- %img{ src: file_raw_path, alt: diff.new_path }
- %p.image-info.hide
- %span.meta-filesize= number_to_human_size(file.size)
- |
- %b W:
- %span.meta-width
- |
- %b H:
- %span.meta-height
-
- .swipe.view.hide
- .swipe-frame
- .frame.deleted
- %img{ src: old_file_raw_path, alt: diff.old_path }
- .swipe-wrap
- .frame.added
- %img{ src: file_raw_path, alt: diff.new_path }
- %span.swipe-bar
- %span.top-handle
- %span.bottom-handle
-
- .onion-skin.view.hide
- .onion-skin-frame
- .frame.deleted
- %img{ src: old_file_raw_path, alt: diff.old_path }
- .frame.added
- %img{ src: file_raw_path, alt: diff.new_path }
- .controls
- .transparent
- .drag-track
- .dragger{ :style => "left: 0px;" }
- .opaque
-
-
- .view-modes.hide
- %ul.view-modes-menu
- %li.two-up{ data: { mode: 'two-up' } } 2-up
- %li.swipe{ data: { mode: 'swipe' } } Swipe
- %li.onion-skin{ data: { mode: 'onion-skin' } } Onion skin
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 45c95f7ab6a..8e5f4d2573d 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -49,7 +49,7 @@
- if discussions_left || discussions_right
= render "discussions/parallel_diff_discussion", discussions_left: discussions_left, discussions_right: discussions_right
- - if !diff_file.new_file && !diff_file.deleted_file && diff_file.diff_lines.any?
+ - if !diff_file.new_file? && !diff_file.deleted_file? && diff_file.diff_lines.any?
- last_line = diff_file.diff_lines.last
- if last_line.new_pos < total_lines
%tr.line_holder.parallel
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index fd4f3c8d3cc..e69c7f20d49 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -12,19 +12,19 @@
- diff_files.each do |diff_file|
- file_hash = hexdigest(diff_file.file_path)
%li
- - if diff_file.deleted_file
+ - if diff_file.deleted_file?
%span.deleted-file
%a{ href: "##{file_hash}" }
%i.fa.fa-minus
= diff_file.old_path
- - elsif diff_file.renamed_file
+ - elsif diff_file.renamed_file?
%span.renamed-file
%a{ href: "##{file_hash}" }
%i.fa.fa-minus
= diff_file.old_path
&rarr;
= diff_file.new_path
- - elsif diff_file.new_file
+ - elsif diff_file.new_file?
%span.new-file
%a{ href: "##{file_hash}" }
%i.fa.fa-plus
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index 5f3968b6709..e8a5e63e59e 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -3,13 +3,13 @@
.suppressed-container
%a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show.
-%table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' }
+%table.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' }
= render partial: "projects/diffs/line",
collection: diff_file.highlighted_diff_lines,
as: :line,
locals: { diff_file: diff_file, discussions: @grouped_diff_discussions }
- - if !diff_file.new_file && !diff_file.deleted_file && diff_file.highlighted_diff_lines.any?
+ - if !diff_file.new_file? && !diff_file.deleted_file? && diff_file.highlighted_diff_lines.any?
- last_line = diff_file.highlighted_diff_lines.last
- if last_line.new_pos < total_lines
%tr.line_holder
diff --git a/app/views/projects/diffs/viewers/_image.html.haml b/app/views/projects/diffs/viewers/_image.html.haml
new file mode 100644
index 00000000000..ea75373581e
--- /dev/null
+++ b/app/views/projects/diffs/viewers/_image.html.haml
@@ -0,0 +1,68 @@
+- blob = diff_file.blob
+- old_blob = diff_file.old_blob
+- blob_raw_path = diff_file_blob_raw_path(diff_file)
+- old_blob_raw_path = diff_file_old_blob_raw_path(diff_file)
+
+- if diff_file.new_file? || diff_file.deleted_file?
+ .image
+ %span.wrap
+ .frame{ class: (diff_file.deleted_file? ? 'deleted' : 'added') }
+ %img{ src: blob_raw_path, alt: diff_file.file_path }
+ %p.image-info= number_to_human_size(blob.size)
+- else
+ .image
+ .two-up.view
+ %span.wrap
+ .frame.deleted
+ %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_content_sha, diff_file.old_path)) }
+ %img{ src: old_blob_raw_path, alt: diff_file.old_path }
+ %p.image-info.hide
+ %span.meta-filesize= number_to_human_size(old_blob.size)
+ |
+ %b W:
+ %span.meta-width
+ |
+ %b H:
+ %span.meta-height
+ %span.wrap
+ .frame.added
+ %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.content_sha, diff_file.new_path)) }
+ %img{ src: blob_raw_path, alt: diff_file.new_path }
+ %p.image-info.hide
+ %span.meta-filesize= number_to_human_size(blob.size)
+ |
+ %b W:
+ %span.meta-width
+ |
+ %b H:
+ %span.meta-height
+
+ .swipe.view.hide
+ .swipe-frame
+ .frame.deleted
+ %img{ src: old_blob_raw_path, alt: diff_file.old_path }
+ .swipe-wrap
+ .frame.added
+ %img{ src: blob_raw_path, alt: diff_file.new_path }
+ %span.swipe-bar
+ %span.top-handle
+ %span.bottom-handle
+
+ .onion-skin.view.hide
+ .onion-skin-frame
+ .frame.deleted
+ %img{ src: old_blob_raw_path, alt: diff_file.old_path }
+ .frame.added
+ %img{ src: blob_raw_path, alt: diff_file.new_path }
+ .controls
+ .transparent
+ .drag-track
+ .dragger{ :style => "left: 0px;" }
+ .opaque
+
+
+ .view-modes.hide
+ %ul.view-modes-menu
+ %li.two-up{ data: { mode: 'two-up' } } 2-up
+ %li.swipe{ data: { mode: 'swipe' } } Swipe
+ %li.onion-skin{ data: { mode: 'onion-skin' } } Onion skin
diff --git a/app/views/projects/diffs/viewers/_text.html.haml b/app/views/projects/diffs/viewers/_text.html.haml
new file mode 100644
index 00000000000..e4b89671724
--- /dev/null
+++ b/app/views/projects/diffs/viewers/_text.html.haml
@@ -0,0 +1,8 @@
+- blob = diff_file.blob
+- blob.load_all_data!(diff_file.repository)
+- total_lines = blob.lines.size
+- total_lines -= 1 if total_lines > 0 && blob.lines.last.blank?
+- if diff_view == :parallel
+ = render "projects/diffs/parallel_view", diff_file: diff_file, total_lines: total_lines
+- else
+ = render "projects/diffs/text_file", diff_file: diff_file, total_lines: total_lines
diff --git a/app/views/projects/hook_logs/_index.html.haml b/app/views/projects/hook_logs/_index.html.haml
new file mode 100644
index 00000000000..6962b223451
--- /dev/null
+++ b/app/views/projects/hook_logs/_index.html.haml
@@ -0,0 +1,37 @@
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Recent Deliveries
+ %p When an event in GitLab triggers a webhook, you can use the request details to figure out if something went wrong.
+ .col-lg-9
+ - if hook_logs.any?
+ %table.table
+ %thead
+ %tr
+ %th Status
+ %th Trigger
+ %th URL
+ %th Elapsed time
+ %th Request time
+ %th
+ - hook_logs.each do |hook_log|
+ %tr
+ %td
+ = render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
+ %td.hidden-xs
+ %span.label.label-gray.deploy-project-label
+ = hook_log.trigger.singularize.titleize
+ %td
+ = truncate(hook_log.url, length: 50)
+ %td.light
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} ms
+ %td.light
+ = time_ago_with_tooltip(hook_log.created_at)
+ %td
+ = link_to 'View details', namespace_project_hook_hook_log_path(project.namespace, project, hook, hook_log)
+
+ = paginate hook_logs, theme: 'gitlab'
+
+ - else
+ .settings-message.text-center
+ You don't have any webhooks deliveries
diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml
new file mode 100644
index 00000000000..2eabe92f8eb
--- /dev/null
+++ b/app/views/projects/hook_logs/show.html.haml
@@ -0,0 +1,11 @@
+= render 'projects/settings/head'
+
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Request details
+ .col-lg-9
+
+ = link_to 'Resend Request', retry_namespace_project_hook_hook_log_path(@project.namespace, @project, @hook, @hook_log), class: "btn btn-default pull-right prepend-left-10"
+
+ = render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml
index 7998713be1f..fd382c1d63f 100644
--- a/app/views/projects/hooks/edit.html.haml
+++ b/app/views/projects/hooks/edit.html.haml
@@ -1,3 +1,4 @@
+- page_title 'Integrations'
= render 'projects/settings/head'
.row.prepend-top-default
@@ -10,5 +11,12 @@
.col-lg-9.append-bottom-default
= form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hook_path do |f|
= render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
+
= f.submit 'Save changes', class: 'btn btn-create'
+ = link_to 'Test hook', test_namespace_project_hook_path(@project.namespace, @project, @hook), class: 'btn btn-default'
+ = link_to 'Remove', namespace_project_hook_path(@project.namespace, @project, @hook), method: :delete, class: 'btn btn-remove pull-right', data: { confirm: 'Are you sure?' }
+
+%hr
+
+= render partial: 'projects/hook_logs/index', locals: { hook: @hook, hook_logs: @hook_logs, project: @project }
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 502220232a1..2cb3045f83e 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -5,12 +5,14 @@
- unless @project.default_issues_tracker?
= content_for :sub_nav do
= render "projects/merge_requests/head"
-= render 'projects/last_push'
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
+
+= render 'projects/last_push'
+
- if @project.merge_requests.exists?
%div{ class: container_class }
.top-area
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index 082a6bcbb2a..7bde839e26f 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -4,7 +4,8 @@
= pipeline_schedule.description
%td.branch-name-cell
= icon('code-fork')
- = link_to pipeline_schedule.ref, project_ref_path(@project, pipeline_schedule.ref), class: "ref-name"
+ - if pipeline_schedule.ref
+ = link_to pipeline_schedule.ref, project_ref_path(@project, pipeline_schedule.ref), class: "ref-name"
%td
- if pipeline_schedule.last_pipeline
.status-icon-container{ class: "ci-status-icon-#{pipeline_schedule.last_pipeline.status}" }
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 075ddc0025c..aea8d13b7c5 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -1,9 +1,5 @@
- failed_builds = @pipeline.statuses.latest.failed
-- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('common_vue')
- = page_specific_javascript_bundle_tag('pipelines_graph')
-
.tabs-holder
%ul.pipelines-tabs.nav-links.no-top.no-bottom
%li.js-pipeline-tab-link
@@ -21,7 +17,7 @@
.tab-content
#js-tab-pipeline.tab-pane
- #js-pipeline-graph-vue{ data: { endpoint: namespace_project_pipeline_path(@project.namespace, @project, @pipeline, format: :json) } }
+ #js-pipeline-graph-vue
#js-tab-builds.tab-pane
- if pipeline.yaml_errors.present?
diff --git a/app/views/projects/pipelines/charts/_overall.haml b/app/views/projects/pipelines/charts/_overall.haml
index edc4f7b079f..0b7e3d22dd7 100644
--- a/app/views/projects/pipelines/charts/_overall.haml
+++ b/app/views/projects/pipelines/charts/_overall.haml
@@ -2,13 +2,13 @@
%ul
%li
Total:
- %strong= pluralize @project.builds.count(:all), 'build'
+ %strong= pluralize @project.builds.count(:all), 'job'
%li
Successful:
- %strong= pluralize @project.builds.success.count(:all), 'build'
+ %strong= pluralize @project.builds.success.count(:all), 'job'
%li
Failed:
- %strong= pluralize @project.builds.failed.count(:all), 'build'
+ %strong= pluralize @project.builds.failed.count(:all), 'job'
%li
Success ratio:
%strong
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 49c1d886423..b39453a50fb 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -7,3 +7,9 @@
= render "projects/pipelines/info"
= render "projects/pipelines/with_tabs", pipeline: @pipeline
+
+.js-pipeline-details-vue{ data: { endpoint: namespace_project_pipeline_path(@project.namespace, @project, @pipeline, format: :json) } }
+
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag('common_vue')
+ = webpack_bundle_tag('pipelines_details')
diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml
index faed65d6588..00bd563999f 100644
--- a/app/views/projects/settings/_head.html.haml
+++ b/app/views/projects/settings/_head.html.haml
@@ -14,7 +14,7 @@
%span
Members
- if can_edit
- = nav_link(controller: [:integrations, :services, :hooks]) do
+ = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do
= link_to project_settings_integrations_path(@project), title: 'Integrations' do
%span
Integrations
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index b51955010ce..f7e410e27b8 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -4,6 +4,7 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
= render "projects/commits/head"
+
= render 'projects/last_push'
%div{ class: container_class }
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 059a0d1ac78..314d8e9cb25 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -3,41 +3,48 @@
.fade-right= icon('angle-right')
%ul.nav-links.search-filter.scrolling-tabs
- if @project
- %li{ class: active_when(@scope == 'blobs') }
- = link_to search_filter_path(scope: 'blobs') do
- Code
- %span.badge
- = @search_results.blobs_count
- %li{ class: active_when(@scope == 'issues') }
- = link_to search_filter_path(scope: 'issues') do
- Issues
- %span.badge
- = @search_results.issues_count
- %li{ class: active_when(@scope == 'merge_requests') }
- = link_to search_filter_path(scope: 'merge_requests') do
- Merge requests
- %span.badge
- = @search_results.merge_requests_count
- %li{ class: active_when(@scope == 'milestones') }
- = link_to search_filter_path(scope: 'milestones') do
- Milestones
- %span.badge
- = @search_results.milestones_count
- %li{ class: active_when(@scope == 'notes') }
- = link_to search_filter_path(scope: 'notes') do
- Comments
- %span.badge
- = @search_results.notes_count
- %li{ class: active_when(@scope == 'wiki_blobs') }
- = link_to search_filter_path(scope: 'wiki_blobs') do
- Wiki
- %span.badge
- = @search_results.wiki_blobs_count
- %li{ class: active_when(@scope == 'commits') }
- = link_to search_filter_path(scope: 'commits') do
- Commits
- %span.badge
- = @search_results.commits_count
+ - if project_search_tabs?(:blobs)
+ %li{ class: active_when(@scope == 'blobs') }
+ = link_to search_filter_path(scope: 'blobs') do
+ Code
+ %span.badge
+ = @search_results.blobs_count
+ - if project_search_tabs?(:issues)
+ %li{ class: active_when(@scope == 'issues') }
+ = link_to search_filter_path(scope: 'issues') do
+ Issues
+ %span.badge
+ = @search_results.issues_count
+ - if project_search_tabs?(:merge_requests)
+ %li{ class: active_when(@scope == 'merge_requests') }
+ = link_to search_filter_path(scope: 'merge_requests') do
+ Merge requests
+ %span.badge
+ = @search_results.merge_requests_count
+ - if project_search_tabs?(:milestones)
+ %li{ class: active_when(@scope == 'milestones') }
+ = link_to search_filter_path(scope: 'milestones') do
+ Milestones
+ %span.badge
+ = @search_results.milestones_count
+ - if project_search_tabs?(:notes)
+ %li{ class: active_when(@scope == 'notes') }
+ = link_to search_filter_path(scope: 'notes') do
+ Comments
+ %span.badge
+ = @search_results.notes_count
+ - if project_search_tabs?(:wiki)
+ %li{ class: active_when(@scope == 'wiki_blobs') }
+ = link_to search_filter_path(scope: 'wiki_blobs') do
+ Wiki
+ %span.badge
+ = @search_results.wiki_blobs_count
+ - if project_search_tabs?(:commits)
+ %li{ class: active_when(@scope == 'commits') }
+ = link_to search_filter_path(scope: 'commits') do
+ Commits
+ %span.badge
+ = @search_results.commits_count
- elsif @show_snippets
%li{ class: active_when(@scope == 'snippet_blobs') }
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index 90ae3f06a98..8d5b5129454 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -15,7 +15,7 @@
%strong= parent.full_path + '/'
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
- pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS,
+ pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
title: 'Please choose a group path with no special characters.',
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
- if parent
diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml
new file mode 100644
index 00000000000..af6a499fadb
--- /dev/null
+++ b/app/views/shared/hook_logs/_content.html.haml
@@ -0,0 +1,44 @@
+%p
+ %strong Request URL:
+ POST
+ = hook_log.url
+ = render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
+
+%p
+ %strong Trigger:
+ %td.hidden-xs
+ %span.label.label-gray.deploy-project-label
+ = hook_log.trigger.singularize.titleize
+%p
+ %strong Elapsed time:
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} ms
+%p
+ %strong Request time:
+ = time_ago_with_tooltip(hook_log.created_at)
+
+%hr
+
+- if hook_log.internal_error_message.present?
+ .bs-callout.bs-callout-danger
+ = hook_log.internal_error_message
+
+%h5 Request headers:
+%pre
+ - hook_log.request_headers.each do |k,v|
+ <strong>#{k}:</strong> #{v}
+ %br
+
+%h5 Request body:
+%pre
+ :plain
+ #{JSON.pretty_generate(hook_log.request_data)}
+%h5 Response headers:
+%pre
+ - hook_log.response_headers.each do |k,v|
+ <strong>#{k}:</strong> #{v}
+ %br
+
+%h5 Response body:
+%pre
+ :plain
+ #{hook_log.response_body}
diff --git a/app/views/shared/hook_logs/_status_label.html.haml b/app/views/shared/hook_logs/_status_label.html.haml
new file mode 100644
index 00000000000..b4ea8e6f952
--- /dev/null
+++ b/app/views/shared/hook_logs/_status_label.html.haml
@@ -0,0 +1,3 @@
+- label_status = hook_log.success? ? 'label-success' : 'label-danger'
+%span{ class: "label #{label_status}" }
+ = hook_log.response_status
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 93c7fa0c7d6..1cf662e29c4 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -9,7 +9,7 @@
- selected = local_assigns.fetch(:selected, nil)
- selected_toggle = local_assigns.fetch(:selected_toggle, nil)
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
-- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"}
+- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"}
- dropdown_data.merge!(data_options)
- classes << 'js-extra-options' if extra_options
- classes << 'js-filter-submit' if filter_submit
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 26567c08eb6..bcfa1dc826e 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -32,7 +32,7 @@
.selectbox.hide-collapsed
- issuable.assignees.each do |assignee|
- = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil
+ = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }
- options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }
diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml
index d23f79be2be..271150ed318 100644
--- a/app/views/shared/issuable/form/_merge_params.html.haml
+++ b/app/views/shared/issuable/form/_merge_params.html.haml
@@ -5,3 +5,13 @@
-# This check is duplicated below, to avoid conflicts with EE.
- return unless issuable.can_remove_source_branch?(current_user)
+
+.form-group
+ .col-sm-10.col-sm-offset-2
+ - if issuable.can_remove_source_branch?(current_user)
+ .checkbox
+ - initial_checkbox_value = issuable.merge_params.key?('force_remove_source_branch') ? issuable.force_remove_source_branch? : true
+ = label_tag 'merge_request[force_remove_source_branch]' do
+ = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
+ = check_box_tag 'merge_request[force_remove_source_branch]', '1', initial_checkbox_value
+ Remove source branch when merge request is accepted.
diff --git a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml
index 8119f19291b..77175c839a6 100644
--- a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml
+++ b/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml
@@ -2,7 +2,7 @@
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder.selectbox
- issuable.assignees.each do |assignee|
- = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { meta: assignee.name }
+ = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { meta: assignee.name, avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }
- if issuable.assignees.length === 0
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' }
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index a7bf610b9c7..1e34b7c1e76 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -18,7 +18,7 @@
.note-header
.note-header-info
%a{ href: user_path(note.author) }
- %span.hidden-xs
+ %span.note-header-author-name
= sanitize(note.author.name)
%span.note-headline-light
= note.author.to_reference
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index 785b1b22a49..5902798dfd0 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -3,24 +3,23 @@
= render 'shared/notes/edit_form', project: @project
-%ul.notes.notes-form.timeline
- %li.timeline-entry
- .flash-container.timeline-content
+- if can_create_note?
+ %ul.notes.notes-form.timeline
+ %li.timeline-entry
+ .flash-container.timeline-content
- - if can_create_note?
.timeline-icon.hidden-xs.hidden-sm
%a.author_link{ href: user_path(current_user) }
= image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40'
.timeline-content.timeline-content-form
= render "shared/notes/form", view: diff_view
- - elsif !current_user
- .disabled-comment.text-center
- .disabled-comment-text.inline
- Please
- = link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
- or
- = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes')
- to post a comment
+- elsif !current_user
+ .disabled-comment.text-center.prepend-top-default
+ Please
+ = link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
+ or
+ = 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})
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index d6ed0e253ad..fe6a49976e0 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -17,6 +17,7 @@ class ProcessCommitWorker
project = Project.find_by(id: project_id)
return unless project
+ return if commit_exists_in_upstream?(project, commit_hash)
user = User.find_by(id: user_id)
@@ -24,8 +25,6 @@ class ProcessCommitWorker
commit = build_commit(project, commit_hash)
- return unless commit.matches_cross_reference_regex?
-
author = commit.author || user
process_commit_message(project, commit, user, author, default)
@@ -76,4 +75,16 @@ class ProcessCommitWorker
Commit.from_hash(hash, project)
end
+
+ private
+
+ # Avoid reprocessing commits that already exist in the upstream
+ # when project is forked. This will also prevent duplicated system notes.
+ def commit_exists_in_upstream?(project, commit_hash)
+ return false unless project.forked?
+
+ upstream_project = project.forked_from_project
+ commit_id = commit_hash.with_indifferent_access[:id]
+ upstream_project.commit(commit_id).present?
+ end
end
diff --git a/app/workers/remove_old_web_hook_logs_worker.rb b/app/workers/remove_old_web_hook_logs_worker.rb
new file mode 100644
index 00000000000..555e1bb8691
--- /dev/null
+++ b/app/workers/remove_old_web_hook_logs_worker.rb
@@ -0,0 +1,10 @@
+class RemoveOldWebHookLogsWorker
+ include Sidekiq::Worker
+ include CronjobQueue
+
+ WEB_HOOK_LOG_LIFETIME = 2.days
+
+ def perform
+ WebHookLog.destroy_all(['created_at < ?', Time.now - WEB_HOOK_LOG_LIFETIME])
+ end
+end
diff --git a/app/workers/system_hook_worker.rb b/app/workers/system_hook_worker.rb
deleted file mode 100644
index 55d4e7d6dab..00000000000
--- a/app/workers/system_hook_worker.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-class SystemHookWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
-
- sidekiq_options retry: 4
-
- def perform(hook_id, data, hook_name)
- SystemHook.find(hook_id).execute(data, hook_name)
- end
-end
diff --git a/app/workers/project_web_hook_worker.rb b/app/workers/web_hook_worker.rb
index d973e662ff2..ad5ddf02a12 100644
--- a/app/workers/project_web_hook_worker.rb
+++ b/app/workers/web_hook_worker.rb
@@ -1,11 +1,13 @@
-class ProjectWebHookWorker
+class WebHookWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
sidekiq_options retry: 4
def perform(hook_id, data, hook_name)
+ hook = WebHook.find(hook_id)
data = data.with_indifferent_access
- WebHook.find(hook_id).execute(data, hook_name)
+
+ WebHookService.new(hook, data, hook_name).execute
end
end