diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000000000000000000000000000000000000..e5636a13783cb5985088231823fc6ff551405b71 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,38 @@ +--- +engines: + brakeman: + enabled: true + bundler-audit: + enabled: true + duplication: + enabled: true + config: + languages: + - ruby + - javascript + eslint: + enabled: true + fixme: + enabled: true + rubocop: + enabled: true +ratings: + paths: + - Gemfile.lock + - "**.erb" + - "**.haml" + - "**.rb" + - "**.rhtml" + - "**.slim" + - "**.inc" + - "**.js" + - "**.jsx" + - "**.module" +exclude_paths: +- config/ +- db/ +- features/ +- node_modules/ +- spec/ +- vendor/ +- lib/api/v3/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5ee22fa6c362ed8104838b1fcf630e04285f4021..dea11bb9f61eee699012bf0b7cf2a784e5713091 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -487,25 +487,6 @@ lint:javascript:report: paths: - eslint-report.html -# Trigger docs build -# https://gitlab.com/gitlab-com/doc-gitlab-com/blob/master/README.md#deployment-process -trigger_docs: - stage: post-test - image: "alpine" - <<: *dedicated-runner - before_script: - - apk update && apk add curl - variables: - GIT_STRATEGY: "none" - cache: {} - artifacts: {} - script: - - "HTTP_STATUS=$(curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=${CI_PROJECT_NAME} --silent --output curl.log --write-out '%{http_code}' https://gitlab.com/api/v3/projects/1794617/trigger/builds)" - - if [ "${HTTP_STATUS}" -ne "201" ]; then echo "Error ${HTTP_STATUS}"; cat curl.log; echo; exit 1; fi - only: - - master@gitlab-org/gitlab-ce - - master@gitlab-org/gitlab-ee - pages: before_script: [] stage: pages diff --git a/.scss-lint.yml b/.scss-lint.yml index 83c68309fa820ed30920f37b84803bc2267e18f9..db234ad739cb4f522c40c9bfed38bffc1167927d 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -11,11 +11,11 @@ linters: # !global, !important, and !optional flags. BangFormat: enabled: false - + # Whether or not to prefer `border: 0` over `border: none`. BorderZero: enabled: false - + # Reports when you define a rule set using a selector with chained classes # (a.k.a. adjoining classes). ChainedClasses: @@ -25,13 +25,13 @@ linters: # (e.g. `color: green` is a color keyword) ColorKeyword: enabled: false - + # Prefer color literals (keywords or hexadecimal codes) to be used only in # variable declarations. They should be referred to via variables everywhere # else. ColorVariable: enabled: true - + # Which form of comments to prefer in CSS. Comment: enabled: false @@ -39,7 +39,7 @@ linters: # Reports @debug statements (which you probably left behind accidentally). DebugStatement: enabled: false - + # Rule sets should be ordered as follows: # - @extend declarations # - @include declarations without inner @content @@ -54,19 +54,19 @@ linters: # more information. DisableLinterReason: enabled: true - + # Reports when you define the same property twice in a single rule set. DuplicateProperty: - enabled: false - + enabled: true + # Separate rule, function, and mixin declarations with empty lines. EmptyLineBetweenBlocks: enabled: true - + # Reports when you have an empty rule set. EmptyRule: enabled: true - + # Reports when you have an @extend directive. ExtendDirective: enabled: false @@ -75,49 +75,49 @@ linters: # when adding lines to the file, since SCM systems such as git won't # think that you touched the last line. FinalNewline: - enabled: false - + enabled: true + # HEX colors should use three-character values where possible. HexLength: enabled: false - + # HEX color values should use lower-case colors to differentiate between # letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`. HexNotation: enabled: true - + # Avoid using ID selectors. IdSelector: enabled: false - + # The basenames of @imported SCSS partials should not begin with an # underscore and should not include the filename extension. ImportPath: enabled: false - + # Avoid using !important in properties. It is usually indicative of a # misunderstanding of CSS specificity and can lead to brittle code. ImportantRule: enabled: false - + # Indentation should always be done in increments of 2 spaces. Indentation: enabled: true width: 2 - + # Don't write leading zeros for numeric values with a decimal point. LeadingZero: enabled: false - + # Reports when you define the same selector twice in a single sheet. MergeableSelector: enabled: false - + # Functions, mixins, variables, and placeholders should be declared # with all lowercase letters and hyphens instead of underscores. NameFormat: enabled: false - + # Avoid nesting selectors too deeply. NestingDepth: enabled: false @@ -129,12 +129,12 @@ linters: # Sort properties in a strict order. PropertySortOrder: enabled: false - + # Reports when you use an unknown or disabled CSS property # (ignoring vendor-prefixed properties). PropertySpelling: enabled: false - + # Configure which units are allowed for property values. PropertyUnits: enabled: false @@ -144,25 +144,25 @@ linters: # be declared with one colon. PseudoElement: enabled: true - + # Avoid qualifying elements in selectors (also known as "tag-qualifying"). QualifyingElement: enabled: false - + # Don't write selectors with a depth of applicability greater than 3. SelectorDepth: enabled: false - + # Selectors should always use hyphenated-lowercase, rather than camelCase or # snake_case. SelectorFormat: enabled: false convention: hyphenated_lowercase - + # Prefer the shortest shorthand form possible for properties that support it. Shorthand: enabled: true - + # Each property should have its own line, except in the special case of # single line rulesets. SingleLinePerProperty: @@ -173,11 +173,11 @@ linters: # individual selector occupy a single line. SingleLinePerSelector: enabled: true - + # Commas in lists should be followed by a space. SpaceAfterComma: enabled: false - + # Properties should be formatted with a single space separating the colon # from the property's value. SpaceAfterPropertyColon: @@ -197,12 +197,12 @@ linters: # colon. SpaceAfterVariableName: enabled: false - + # Operators should be formatted with a single space on both sides of an # infix operator. SpaceAroundOperator: enabled: true - + # Opening braces should be preceded by a single space. SpaceBeforeBrace: enabled: true @@ -210,7 +210,7 @@ linters: # Parentheses should not be padded with spaces. SpaceBetweenParens: enabled: false - + # Enforces that string literals should be written with a consistent form # of quotes (single or double). StringQuotes: @@ -241,7 +241,7 @@ linters: # be unnecessary. UnnecessaryParentReference: enabled: false - + # URLs should be valid and not contain protocols or domain names. UrlFormat: enabled: true diff --git a/Gemfile.lock b/Gemfile.lock index 873cd8781ef0c4ec5c34f78696f10185ab646acd..dd2c85052f3226cb056841b90059e40b1ea7ecb1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -341,7 +341,7 @@ GEM grape-entity (0.6.0) activesupport multi_json (>= 1.3.2) - grpc (1.2.5) + grpc (1.3.4) google-protobuf (~> 3.1) googleauth (~> 0.5.1) haml (4.0.7) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 2090a7e12d60445b1cb5e220dd589538654723e9..c5fffea8bb02da198100dc1b44e7015e90f90b2b 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 24c423dd01ef508551a6ed54ac3adbe0cbc7415b..d34561e5512af18363689c53af607e955f3cf2bb 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 f1b0740867199638c9ab823e61315c6a7e325247..573940979441ae64b553e2525f5c85c384dee821 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 22032d0f91414d1a3e2c9740dad39f21ce2ab023..894ed81b044c5a989222881a414579ee82ce61a7 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 b0b1cfd6c8aa8794921a1b728dd7c7f823dcdfe2..0ca7cabfc5a2e7ad94b4578549354d75ffd0dbaf 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 14c98847d933555c28bd3756c70926bf274e4871..77cbaeb43eff80020af294bfe1ce2b219638c414 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 b7a6b5d847977d0ef155c5d370403b543242757b..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..5aab25e03489cdcb02565e1fc5bfc56ed64d180c --- /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 0000000000000000000000000000000000000000..b9a6d5ca5fca85df218c8615bddc4a5c5bf860e1 --- /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 86ab50d8f1e9742196033a6365141bf68565ef0b..052e34a8aef8a94f409e83fef874dcd17eac9f0a 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 bacb26734c9031297ea7a8f34dec01c259aa93c9..c44892dae3d21cce0ae299479fd0e399774e87f2 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 aea3592c6badc9010b60dfe38477cbc323a2e266..ec45253e50bddce19c20d0c0d30232ad86c267e9 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 486b13e60af178a8a03a25c38813e5735f01e747..8155218681c8e6366083f2d0003e0e3dc3c0d544 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 d866d4e94b07517088187749ac344bc9b2fee718..fcd4fdaf09f64a97dd1e74939c52a2677e3980ae 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 c07bd25e6fd954a39b1ea1ffef388e2e81a704a5..dc4b21950502864fc59e88f6ecd5e464d8c56402 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 0000000000000000000000000000000000000000..fd0dcd716d6b97cc7adb9ee51e8e6dd7b992a77d --- /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 0000000000000000000000000000000000000000..af2b4c6786ed91b6ae040da88a1c7d56384f96fc --- /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 0000000000000000000000000000000000000000..20f63ab663c44aae50cbacabe71811972235dad8 --- /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 d2ec1791d2bbd4e7381cda490ca90d1efb0e47f8..b8ba77f45134db00faeddbafe5ce83dd97fe6f14 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 d64b1237b2ce6a57348d6af87737941e8df000f6..75907c35b7e900fcc35c6328412c35fa9980ccf5 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 3dec911d2891cc35fbbc1101e5ded4df3e130901..fefe5575d9bec1b77bb639006c8debfd67833875 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 d86ae57cd9ac577dc8b1a04db656c7cb6b275857..2d6bc17d4ff430f2cc7b8be631b3ec6b543bf492 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 f8674b763c8928e5388aec5d0f09b8b1c1046cc2..78f425057eb495f6ae9f574efafea7a651b40031 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 eadb9409fee692d45d7967ceaf6b1175e173d34e..25b4feca3c3a08aaad5cb52a6c2f3f7abc4dbe68 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 d76053fe72aecf05661db0fa5fde6d3bef53dd2f..491636535489a563757d2447b13be78f48b188eb 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 0000000000000000000000000000000000000000..d349e3fad9ce01dec8144177b386914309ec8ded --- /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 9ab17e67d4c542c6a8137c6af128c37b2344a7d0..5ae833cd5f6914e6545e6658e9a0cc66d9018e8e 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 1fd734d279b0dbdf3bafa7272b40b9e994fcf4c0..cec3b54d5678ac7b72f6c3ad290629f8a971dc93 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 0c3407f34f884fa3d9498bb9d14ff3043734e797..785b09e622ffe58473a3bd3cc568decb9478e0d5 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 17a4e8fd83e3b51157e78bf0cb2fcd5b65e31296..4db77752c0cceef6fb897e079e2bf2fe07c1808f 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 68d7ab4bf8411d66d4cd383430034b1b9e9fd996..ebe662136d5e458b33e0eb2a4881773e72561617 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 9064383239097e0f2cec119d2817f64c24c9ab72..7b4eb689f1b40f4c7add4bae43c7cca062aedcf6 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 cfb1df4df8431e9bd3f8ff9f30992d511598d5c3..b58922626fa72fc90dd2a88f3507aa2679609e5a 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 48d3b7b1d07e6ada1e1c8b893811d58329d4d248..f269d53093d6ddf2b5fbb6534b3be48e222d5c2c 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 d79ae47f589f224433931c0508711c678f98e9cf..c2346f2f1c353886268f0830536b7488154bc94c 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 bee9b13b375892c96e34cdb3420eb54a3ebda04b..702e76625271506b3f59d2783f6299a2c9c2241b 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 8dbac76e30ad5b1b62122a889871bd43efd494c8..971d54e7472ddc44536368bb792f6d88119f7d8f 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 49e453c7dbcf1392791a6526bbb62a16066c8cbf..875e47cdff3de75a48a797545b62cf0043a0f4e9 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 51918917329e7e9c6b925821b813e114107ba09f..cffd3b6060daca875b695de507365003807f1625 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 292584eba28b8320546d3fa171f394a59b01ee72..cf2e565dd2d99d2fbda11a1d33b30f8aee808f1e 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 f0bf3d4c267c286ffc55172dcd0d0a584b195cc9..24ab2bedea24fdb1635b8229665aef85787a09d6 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 0000000000000000000000000000000000000000..aa069b89563e4c73e6286329061c894a2b12321e --- /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 ccfe553c89e0ed164a175f46913922e86e7a3b9d..b9251e140f8f1ae168d7e6fcff5aed1800727f74 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 ab5aed249174bd5f10546aaeb6092977a3da9c27..47ce21d238ba7e3d4cac21b49bb7b47e409492af 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 e2f5aa8508ecc3bede5636a2ab6adebb8aa9e77f..907717dcb960510510d4b79a32a0d9948d979b06 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 1efa9fe060f53ff8f8650b83cc91747c0994a4e2..d5388c4cd20ea45791abaf32d50497de56c2d9b1 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 0000000000000000000000000000000000000000..846cd60518f16dd075da864a4dcf9539ba6b5dca --- /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 5a1efcab1a3f9483f7a33f891adc39a76a7c00af..3d49ea975918f15c14fd4ae2d5e2877d720ae9d4 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 79d420a32d3d81c71e9ffed27a378bcfe294ee4f..6195121b931b16d946385cb52143dd6fd9ca87e8 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 965ced4d37252344f0344a9d06a433a2c2f0763e..18a2d69db293f3735ee2241ddca8c182631fd087 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 008d2f5815f5c5944c84996268fe4f6e7253c37b..88dd600e5fef9ca1758b2c5a4c03e5f2f79e8dc2 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 0000000000000000000000000000000000000000..354f0d6db3a356ba7ebc9723a1d68d9ff1541769 --- /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 86d13a0d2226a5e99cccc25bfd4e73a1c84f60de..38bd82841dc68d16aa9c8ecc49f31ed471caa9d1 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 0352065998b1913d07063b7d3a3442a340a2bc0c..314906b5f09b54aa586819c85076817c80a4c192 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 667f4870c7a343f63e2f50e279d4b1dda1e71140..2a0b58fae7c4e28a9cf6a28b0f35fc797e0eaf4c 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 d59d51905a6de42c5320676a1ecdc4976b3a77a9..5b5cdebe9197119d8dbf0e77cedfb0abac10ad67 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 4a06ee653ee0b6b46fd95d279324649fe18bf8d9..4c4fbdd4d390f8042290c9ed116d0ea620fafe95 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 e5b1e6e8bc70d0eba506444c26e9af2ad717affc..4e6e68059204c1a9f05f7b7a27d91315ec2d32d0 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 98bbcfaaba5f049b4e6b1ebb1ba6f8f9cc5b3e5e..654aa1a6533b4e0c15a3bd97b23d4301b2ac2968 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 09b73eee8cf901cde3430da78ba038ef54d45bf0..c0763a8a9c40d4ef1a56ed794fc96dac4a60ac06 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 d2980db218af601276b7767460d661372ccee789..654468bc7fe478d014dc9d63149a3b2dd026e895 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 cf6e53c4ca4c6ff28d31c2cabad892da51c25f51..07213ca608a956b70ee44e9ede192975f93de559 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 6359f7596b1d5af26049cdb612eb407d2855304a..f734952fa6cce7a947a662a0f8e6071281cbbb4c 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 c4463abdfe6bf79c62ae8fb3d8804119df85363b..63d02b76f6b67b5768f3169ba5e15c1c476506ed 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 50a1d7fc3e1f85d7619ce85bdf88296905b67688..58194b0ea1371e09a7e0cbb23468f748bd3e7650 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 1764004078e3e8dfb85a5bb2c3fa11c7fafa4eb5..2a4cff3756695514ab6f2bf5cf675f5f1c56d2b5 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 6aab477f431054aadeb0ff5e38208f0d63653995..be944da5a67db08bd054f02776d04efc7743e60a 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 eef24052a064fa5248f56317b9df8b5b00b7401b..40e43c27f91384ac4cca3ec19893f5c564a14971 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 c645805c6dab13eef742a7d55830e8aa55c9efcb..1584235ab003bf9cd8accedb7f3bc0a1515bacc8 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 a165fdc312f1e9b0eb564e3e9ac76e7d6dca5308..7503f3739c31c0ef1a2a5bdedc963129b11fbbd4 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 0000000000000000000000000000000000000000..2738b229d847b880f628c417dd46dda44567b353 --- /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 ddddb6bdf8fa60471c77b295b8bd8755cdfc8dd0..074239702f82b9ea4a1a51493f4abc615bde93f1 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 d7c627432d228dfe86abd963684df8be23e1a0a7..ebf8fb92ab5df1fe62ccedf80b7cd7fe6aaee302 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 2eec013fa9d69dcdbcec873278fbf22b2291990f..356af776b8d39ec3581724512af54b1c0878fed3 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 6e3917a10a3daba6462838f636354cb38d1db460..1bd61c1d465e91eb8c0a491e9f83d5549c278f02 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 c06bfe0ccdd773823f8093ec1469373740767d69..b04bed4c0147679a04f948771886c3a95047a7f3 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 4d59267f71d7336e6175c7bc9480bd73ecb70fe3..aebee06d560b1bb54d65c40344d68f676f264a80 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 cfca0dcd2f212e7f752ca065bfcaade2e580bcec..29af57d76643238d7b1410c8fd6a5f3947386942 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 4c7f4f5a4291856dc8002c0068376fad4b07d865..def0967525378578a30d2013ced9c5a0a8514761 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 a91a986e195e89e41fcc253f3bd5889280b93b36..fe8696238331378b7ce07ba698c5d9116f075974 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 b2494a0be6e9ca9904167edca1ac81d61c30e1b1..8977a7cdafe397d2d94257d53132f91139d97f5c 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 cf3914568a6e2c8ba2e9bad66aa524a9743b2863..3f816a250c292c5bfcf8bf647db0fd1ae463388b 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 b3247ae36ddeb828fa4af343f07ec3a30e267479..f7eb75395b5f0cc690827ce1574559a59814adea 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 f1030912c68750e348df38be54803eaa73735963..85c616ca576d56cf63961e5c37efe66f9d94c0bb 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 40fbe354492842ecc67d26710c3b717f0e7c946c..80ea6312768bc9c662806938a0d14d702fc19004 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 f2053bda83aadbb4dbcb8766999f404d02b6c7bc..2ffc989ed71c984d40445c408caf374c8b2ca465 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 e8fb1b597527cb1088988cbefff626bec2cdff94..f0d998731d75be214ac3eb1dc098d35857e5fc04 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 54b19e6d65159d36bf11018ea915bda414084950..f2fddf7f3453e0693fe5ae27754650081cdc1f88 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 22736c717254967095d5053524e626280afe7443..1d4d03a8b7db22205323b2bb9b49a8e0a724277d 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 8f6f5b937c4b42d1f3521fe93ce5314365d8b5f7..3e07b8110277d0c0118da081ca21bf1ba0ec4661 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 0000000000000000000000000000000000000000..4241b912d5bab63c7336101827a3606a7e925e1f --- /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 8d4d7180baf2ecce7927fd29309108f45bcaf622..6819886ebf42acd8bfcf29fb0926e4c9247b1c9d 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 0000000000000000000000000000000000000000..7dd9943190f8325baee482a13e12655478b8104a --- /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 0000000000000000000000000000000000000000..56127bacda2a2d654ea47b1122a68b919f695f24 --- /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 0777f5e262971656f1409735fffcfde6748d43db..0e35a1905bf5b91792de0d0883ab4e351471b0c8 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 2e5f120c4e4ca500ee2f54070bef95b799e64886..9b9559c7fe5bece518a02c1bb640cb9a93d1f35f 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 e1b270a08c25b4b8ea1ac270ce9df6e1fb30ebc2..a676eba2aee848517df130dd99558a71b5e360a0 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 190ad4b40a5c698d329b2a357995ca6694894e51..f893c3e1675604e3d15ce1d83fb76d8e6e697c1a 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 596499230f9c75308a48fae42f3b9f4899847bfe..3b2a555a1439bc6250c20015383137159fcab9f6 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 162ae153b1c53b5856e911e0f979038129bf33f7..99efe9c9b866f701d2e8f18a8060e0c309685dbc 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 a2f6a7ab1cb5dad46f97dbb46590c681f9c2daed..d696577278d155a8c00f940a14f1f90435493bb2 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 c3f55ff821fcd72506c794d84e594c31e1a45cfc..70042dee20f1cd2b38152a3eee74427a6bb13f58 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 7ba3f3f6c42a5d44b213ed5bf60d92f9d217d671..db5ab9399482d2f7398fe003fef9b5832188554c 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 1584695a62b68138fa4ef72d888592eec31eb959..0000000000000000000000000000000000000000 --- 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 c0943100ae3fc89450d91469a93e0080c957fb93..769ac655d0a54f36f419f2694d78bdf4425de0cf 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 d7851c7999033fb263b524879c427743ed7b39df..fd6e7111f38b5c4f1681caa9cad28c336f468879 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 873504099d4c0d5b51c23160d0464351d3dcb5c7..0f63774fb9be9c0244426f9c09795f84b73a2ca9 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 b2097e8874102129e2cac59e76270d2818903413..35b75bc09237c9fcd60b45410d364ae8d5ede93e 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 18997baa998908e5cf484b66e978706102f16d1c..80a8ba4a755e717917f34fa4143802e05a37984d 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 d068c895fa39059d53380dab110f3594b644b8b1..86779eeaf15161d957b7e5501caff89e8441d6d4 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 02eb7c8462c58941e76d88eef325731a8a121c9a..546376aeed80717119ed76d5209420933e41f3ab 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 − - = diff.old_path - - elsif diff.renamed_file - = diff.old_path + = diff_file.old_path + - elsif diff_file.renamed_file? + = diff_file.old_path → - = diff.new_path - - elsif diff.new_file + = diff_file.new_path + - elsif diff_file.new_file? %span.new-file + - = 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 → diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index 5ac23aa3997f678e11161a8dfb66bfe23394731d..895d8807e47e40fccba060785a4231a935e82b6f 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 f5bb7364d4ada84dbf1f7f502a4af9a90586fe72..10f581d751b690aa6ac123b420977de4fae4c198 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 f8a6e98d2804a1cd70460b0efd77b157ad9a4908..e8b1940af2dcbcfa24bfa6cf3f45ac354f0b4454 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 27c8e3c7fca68ddf4b48ba1eb0fb743324c8e747..ef8d8051cbf40a679fcbb1e94a05bdde3d0bbcd7 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 67f57b5e4b905d2e047fd09439957b05414cd32b..41f75a491a54e3628c3d7438d0d3de6c0da7c87a 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 48f8c6560800957d73c3d5ad829236d462287562..e8db868f49b2ef66e8a12a85c9c5898dffb3e972 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 c781e423c4d6571e6a9188a035cd0a3d9c7ec9b8..c7e22a0b4ec909dd671c324c74d28b5e0d7faa4d 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 71a1b9e6c05edb437b11ff1841223de83cf6d472..4768438c29e58b0e9c24845026b853897d4f5911 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 f22b385fc0f632f500db84f73c53647163a60f0d..b5aea217384daa22c2e09934e87f8f3ece010ccb 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 4e4fdb73ae3a6f8b5a840ed300360ba249097233..73c316472e35d6b7d68c38c24b4f5a36baaf2fff 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 ca10921c5e2e485cddc1fd543b898cd662887c66..0000000000000000000000000000000000000000 --- 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 45c95f7ab6ab1b0918ec61a75062ec6e89461376..8e5f4d2573d65bc0444fe522410ee7be9e3ef436 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 fd4f3c8d3ccc641e06833cf154e0253558abf483..e69c7f20d49848c9c2e0288c5f8483cbc1e2b341 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 → = 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 5f3968b67096aca4d5d43ce8acd3ed6452ecfab5..e8a5e63e59e37ec05cdd99a0b881a7c5a5db1f71 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 0000000000000000000000000000000000000000..ea75373581e68e9b9d3efe0a0fc77e6e5fced408 --- /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 0000000000000000000000000000000000000000..e4b896717244807f1b98dfbd80265bb879f27617 --- /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 0000000000000000000000000000000000000000..6962b22345104ec393c0ed1d0c42e2bbbd0692ee --- /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 0000000000000000000000000000000000000000..2eabe92f8eb491fe936f6a60194e5668fd220c76 --- /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 7998713be1f23e98df7a6f14d2a159c4c42365d7..fd382c1d63fc2c0f20bc86a38f630cfa7f2964d6 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 502220232a18537873d71e7e6be01ec05558d19b..2cb3045f83e28924c603561145f2d1a5a754d3e2 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 082a6bcbb2a8fbfdffecb72226a7960f5ce81652..7bde839e26f29591d858081e91d196476ad9bdfc 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 075ddc0025c765f005279325b9bcdd5cc6da5a88..aea8d13b7c51361a6a05712be3607e2766167114 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 edc4f7b079fc27457ad5865fae0e06f92ee0e253..0b7e3d22dd73de3d46a7eaa6a119bec65ce3e40f 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 49c1d88642329752938c110107f3d85563c4383a..b39453a50fbcb7e099817b557642b0cab99d9d15 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 faed65d65885ff09a375d0066b60203c6ca8e4f3..00bd563999f7acaa42933c5cd171ad3c094f0b4f 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 b51955010ce9cb4545c3a845e0e99c9bcaa52285..f7e410e27b8d9e79a11459bb06b43c05b1c59e3a 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 059a0d1ac789c79f7cdad9aad8ed8505602f1f0d..314d8e9cb25ba25315030ceed41562130d1e8ae6 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 90ae3f06a98cce1d582e4f89ff94a493e4974b3c..8d5b5129454aa6d4bc31a6e693a2c84363b11f30 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 0000000000000000000000000000000000000000..af6a499fadb065e0d0d089a904b90d95a12a067c --- /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 0000000000000000000000000000000000000000..b4ea8e6f9524f3d787a43c86a0b16e9ade3a6bdc --- /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 93c7fa0c7d65d29d32048c4002547a0b60cd14d3..1cf662e29c4ce4e04c585f74c4da80c698a04ffa 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 26567c08eb6f0723de21722dee701604283da276..bcfa1dc826e6bc2b02ee68b6c8dda48cf62bc06f 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 d23f79be2be9448f3d53571288b5bc19fc4a4062..271150ed318d11ff7c082718a5badab62e5ee00b 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 8119f19291b9be8a438bbeed0f34ba1df8c150fe..77175c839a60bfb4d457f8fa058a30a3aaab99dd 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 a7bf610b9c709e41089766c0b3094a9c7569ccea..1e34b7c1e76acd2bef84930de58a4e8a2403ff5e 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 785b1b22a4971204b6cff9c261bbf87611050088..5902798dfd06119d5957412a7866969cda71e707 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 d6ed0e253ad8be709294fa30ba0e0164c5a94c43..fe6a49976e0c1196a89b4e81e45e78fc6bd49f3b 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 0000000000000000000000000000000000000000..555e1bb86919dc8ad0e2b3925cada873038e23ec --- /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 55d4e7d6dab99f7a27075268841cbb24ca089be7..0000000000000000000000000000000000000000 --- 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 similarity index 62% rename from app/workers/project_web_hook_worker.rb rename to app/workers/web_hook_worker.rb index d973e662ff22d07f34b48fff95442043187ffe2f..ad5ddf02a122ee043b64e18e1c50fd0b3cb1fc47 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 diff --git a/changelogs/unreleased/12614-fix-long-message-from-mr.yml b/changelogs/unreleased/12614-fix-long-message-from-mr.yml new file mode 100644 index 0000000000000000000000000000000000000000..30408ea4216a3d4fd38c692167a18900f1557380 --- /dev/null +++ b/changelogs/unreleased/12614-fix-long-message-from-mr.yml @@ -0,0 +1,4 @@ +--- +title: Implement web hook logging +merge_request: 11027 +author: Alexander Randa diff --git a/changelogs/unreleased/23036-replace-all-spinach-tests-with-rspec-feature-tests.yml b/changelogs/unreleased/23036-replace-all-spinach-tests-with-rspec-feature-tests.yml new file mode 100644 index 0000000000000000000000000000000000000000..b350b27d863a4dd8f192ccbbcb31ac4c467cb9d0 --- /dev/null +++ b/changelogs/unreleased/23036-replace-all-spinach-tests-with-rspec-feature-tests.yml @@ -0,0 +1,4 @@ +--- +title: Replace 'starred_projects.feature' spinach test with an rspec analog +merge_request: 11752 +author: blackst0ne diff --git a/changelogs/unreleased/27439-memory-usage-info.yml b/changelogs/unreleased/27439-memory-usage-info.yml new file mode 100644 index 0000000000000000000000000000000000000000..dd212853f57233bad045fbc9abeb548ce80b590a --- /dev/null +++ b/changelogs/unreleased/27439-memory-usage-info.yml @@ -0,0 +1,4 @@ +--- +title: Add performance deltas between app deployments on Merge Request widget +merge_request: 11730 +author: diff --git a/changelogs/unreleased/30917-wiki-is-not-searchable-with-guest-permissions.yml b/changelogs/unreleased/30917-wiki-is-not-searchable-with-guest-permissions.yml new file mode 100644 index 0000000000000000000000000000000000000000..c9bd2dc465e108c0844f95b94fc0c06457487241 --- /dev/null +++ b/changelogs/unreleased/30917-wiki-is-not-searchable-with-guest-permissions.yml @@ -0,0 +1,4 @@ +--- +title: 'Fix: Wiki is not searchable with Guest permissions' +merge_request: +author: diff --git a/changelogs/unreleased/31448-jira-urls.yml b/changelogs/unreleased/31448-jira-urls.yml new file mode 100644 index 0000000000000000000000000000000000000000..d0e39f61b55599191560db697c1443cd02ddf0dc --- /dev/null +++ b/changelogs/unreleased/31448-jira-urls.yml @@ -0,0 +1,4 @@ +--- +title: Add API URL to JIRA settings +merge_request: +author: diff --git a/changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml b/changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml new file mode 100644 index 0000000000000000000000000000000000000000..4137050a0771eaca8375ebbc4d063f11fbe2e23a --- /dev/null +++ b/changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml @@ -0,0 +1,4 @@ +--- +title: Fix the last coverage in trace log should be extracted +merge_request: 11128 +author: dosuken123 diff --git a/changelogs/unreleased/31616-add-uptime-of-gitlab-instance-in-admin-area.yml b/changelogs/unreleased/31616-add-uptime-of-gitlab-instance-in-admin-area.yml new file mode 100644 index 0000000000000000000000000000000000000000..6dc48d6b2d8be85a9c44e1f85b9500df0b75e5fa --- /dev/null +++ b/changelogs/unreleased/31616-add-uptime-of-gitlab-instance-in-admin-area.yml @@ -0,0 +1,4 @@ +--- +title: Add server uptime to System Info page in admin dashboard +merge_request: 11590 +author: Justin Boltz diff --git a/changelogs/unreleased/31849-pipeline-show-view-realtime.yml b/changelogs/unreleased/31849-pipeline-show-view-realtime.yml new file mode 100644 index 0000000000000000000000000000000000000000..838a769a26ed870f0bc5ae6c915bc7ef38e6e1ad --- /dev/null +++ b/changelogs/unreleased/31849-pipeline-show-view-realtime.yml @@ -0,0 +1,5 @@ +--- +title: Creates a mediator for pipeline details vue in order to mount several vue apps + with the same data +merge_request: +author: diff --git a/changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml b/changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml new file mode 100644 index 0000000000000000000000000000000000000000..a58f3a7429e36e412ba74c9169a4166470a7f74c --- /dev/null +++ b/changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml @@ -0,0 +1,4 @@ +--- +title: Fix pipeline_schedules pages throwing error 500 +merge_request: 11706 +author: dosuken123 diff --git a/changelogs/unreleased/32851-postgres-min-version.yml b/changelogs/unreleased/32851-postgres-min-version.yml new file mode 100644 index 0000000000000000000000000000000000000000..139307d65c6ae216da850a7932f3b67328689e2d --- /dev/null +++ b/changelogs/unreleased/32851-postgres-min-version.yml @@ -0,0 +1,4 @@ +--- +title: Minimum postgresql version is now 9.2 +merge_request: 11677 +author: diff --git a/changelogs/unreleased/ci-build-pipeline-header-vue.yml b/changelogs/unreleased/ci-build-pipeline-header-vue.yml new file mode 100644 index 0000000000000000000000000000000000000000..2bbff2fdd161cb02e9a92ce66e5afbfb3f9b4fcf --- /dev/null +++ b/changelogs/unreleased/ci-build-pipeline-header-vue.yml @@ -0,0 +1,4 @@ +--- +title: Creates CI Header component for Pipelines and Jobs details pages +merge_request: +author: diff --git a/changelogs/unreleased/dm-consistent-last-push-event.yml b/changelogs/unreleased/dm-consistent-last-push-event.yml new file mode 100644 index 0000000000000000000000000000000000000000..acc17cb45236bb742330395a62a73345dae9d7e9 --- /dev/null +++ b/changelogs/unreleased/dm-consistent-last-push-event.yml @@ -0,0 +1,4 @@ +--- +title: Consistently display last push event widget +merge_request: +author: diff --git a/changelogs/unreleased/dm-more-dependency-linkers.yml b/changelogs/unreleased/dm-more-dependency-linkers.yml new file mode 100644 index 0000000000000000000000000000000000000000..12d45e71e859b3fdb4e36b3ee014412f3a4e379b --- /dev/null +++ b/changelogs/unreleased/dm-more-dependency-linkers.yml @@ -0,0 +1,4 @@ +--- +title: Autolink package names in more dependency files +merge_request: +author: diff --git a/changelogs/unreleased/fix-counter-cache-for-acts-as-taggable.yml b/changelogs/unreleased/fix-counter-cache-for-acts-as-taggable.yml new file mode 100644 index 0000000000000000000000000000000000000000..e40668546c0d05b33d898995cf499dcbabdf748a --- /dev/null +++ b/changelogs/unreleased/fix-counter-cache-for-acts-as-taggable.yml @@ -0,0 +1,4 @@ +--- +title: Fix counter cache for acts as taggable +merge_request: +author: diff --git a/changelogs/unreleased/fix-terminals-support-for-kubernetes-service.yml b/changelogs/unreleased/fix-terminals-support-for-kubernetes-service.yml new file mode 100644 index 0000000000000000000000000000000000000000..fb91da9510c2580e3e924cdcf2c7eb7dfbe26973 --- /dev/null +++ b/changelogs/unreleased/fix-terminals-support-for-kubernetes-service.yml @@ -0,0 +1,4 @@ +--- +title: Fix terminals support for Kubernetes Service +merge_request: +author: diff --git a/changelogs/unreleased/issuable-form-create-label-sub-groups.yml b/changelogs/unreleased/issuable-form-create-label-sub-groups.yml new file mode 100644 index 0000000000000000000000000000000000000000..54b818d6d5e34e23fbf2baa37908d9de14cb7e45 --- /dev/null +++ b/changelogs/unreleased/issuable-form-create-label-sub-groups.yml @@ -0,0 +1,4 @@ +--- +title: Fixed create new label form in issue form not working for sub-group projects +merge_request: +author: diff --git a/changelogs/unreleased/issue_19262.yml b/changelogs/unreleased/issue_19262.yml new file mode 100644 index 0000000000000000000000000000000000000000..7bcbc647fcbeb4051793269ffb4e780a167a3431 --- /dev/null +++ b/changelogs/unreleased/issue_19262.yml @@ -0,0 +1,4 @@ +--- +title: Prevent commits from upstream repositories to be re-processed by forks +merge_request: +author: diff --git a/changelogs/unreleased/rework-authorizations-performance.yml b/changelogs/unreleased/rework-authorizations-performance.yml new file mode 100644 index 0000000000000000000000000000000000000000..f64257a6f56c65301cda516f5f987ddf505f5b31 --- /dev/null +++ b/changelogs/unreleased/rework-authorizations-performance.yml @@ -0,0 +1,6 @@ +--- +title: > + Project authorizations are calculated much faster when using PostgreSQL, and + nested groups support for MySQL has been removed +merge_request: 10885 +author: diff --git a/changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml b/changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml new file mode 100644 index 0000000000000000000000000000000000000000..d633995d4676eb0706fe6c63654d1e962ef77783 --- /dev/null +++ b/changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml @@ -0,0 +1,4 @@ +--- +title: Strip trailing whitespaces in submodule URLs +merge_request: +author: diff --git a/config/initializers/acts_as_taggable.rb b/config/initializers/0_acts_as_taggable.rb similarity index 63% rename from config/initializers/acts_as_taggable.rb rename to config/initializers/0_acts_as_taggable.rb index c564c0cab11342ae8f7f2977cedd13c44667b73d..54e9fcc31dbf72eebb5dc175d158f4e71ce1d5c2 100644 --- a/config/initializers/acts_as_taggable.rb +++ b/config/initializers/0_acts_as_taggable.rb @@ -3,3 +3,7 @@ ActsAsTaggableOn.strict_case_match = true # tags_counter enables caching count of tags which results in an update whenever a tag is added or removed # since the count is not used anywhere its better performance wise to disable this cache ActsAsTaggableOn.tags_counter = false + +# validate that counter cache is disabled +raise "Counter cache is not disabled" if + ActsAsTaggableOn::Tagging.reflections["tag"].options[:counter_cache] diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 5a90830b5b3c99b26619c2cd4c1cfbdc76509ef5..4fb4baf631f9ef8479b26d333f5db7ff5ebf842e 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -368,11 +368,14 @@ Settings.cron_jobs['gitlab_usage_ping_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['gitlab_usage_ping_worker']['cron'] ||= Settings.__send__(:cron_random_weekly_time) Settings.cron_jobs['gitlab_usage_ping_worker']['job_class'] = 'GitlabUsagePingWorker' -# Every day at 00:30 Settings.cron_jobs['schedule_update_user_activity_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['schedule_update_user_activity_worker']['cron'] ||= '30 0 * * *' Settings.cron_jobs['schedule_update_user_activity_worker']['job_class'] = 'ScheduleUpdateUserActivityWorker' +Settings.cron_jobs['remove_old_web_hook_logs_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['remove_old_web_hook_logs_worker']['cron'] ||= '40 0 * * *' +Settings.cron_jobs['remove_old_web_hook_logs_worker']['job_class'] = 'RemoveOldWebHookLogsWorker' + # # GitLab Shell # diff --git a/config/initializers/fast_gettext.rb b/config/initializers/fast_gettext.rb index a69fe0c902e808418057341386c5b2ec1b2d9705..eb589ecdb529f2d68f457378346e3f8786c371b4 100644 --- a/config/initializers/fast_gettext.rb +++ b/config/initializers/fast_gettext.rb @@ -1,5 +1,6 @@ FastGettext.add_text_domain 'gitlab', path: File.join(Rails.root, 'locale'), type: :po FastGettext.default_text_domain = 'gitlab' FastGettext.default_available_locales = Gitlab::I18n.available_locales +FastGettext.default_locale = :en I18n.available_locales = Gitlab::I18n.available_locales diff --git a/config/initializers/postgresql_cte.rb b/config/initializers/postgresql_cte.rb new file mode 100644 index 0000000000000000000000000000000000000000..7f0df8949dbf11b43911add85e256732977a8875 --- /dev/null +++ b/config/initializers/postgresql_cte.rb @@ -0,0 +1,132 @@ +# Adds support for WITH statements when using PostgreSQL. The code here is taken +# from https://github.com/shmay/ctes_in_my_pg which at the time of writing has +# not been pushed to RubyGems. The license of this repository is as follows: +# +# The MIT License (MIT) +# +# Copyright (c) 2012 Dan McClain +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +module ActiveRecord + class Relation + class Merger # :nodoc: + def normal_values + NORMAL_VALUES + [:with] + end + end + end +end + +module ActiveRecord::Querying + delegate :with, to: :all +end + +module ActiveRecord + class Relation + # WithChain objects act as placeholder for queries in which #with does not have any parameter. + # In this case, #with must be chained with #recursive to return a new relation. + class WithChain + def initialize(scope) + @scope = scope + end + + # Returns a new relation expressing WITH RECURSIVE + def recursive(*args) + @scope.with_values += args + @scope.recursive_value = true + @scope + end + end + + def with_values + @values[:with] || [] + end + + def with_values=(values) + raise ImmutableRelation if @loaded + @values[:with] = values + end + + def recursive_value=(value) + raise ImmutableRelation if @loaded + @values[:recursive] = value + end + + def recursive_value + @values[:recursive] + end + + def with(opts = :chain, *rest) + if opts == :chain + WithChain.new(spawn) + elsif opts.blank? + self + else + spawn.with!(opts, *rest) + end + end + + def with!(opts = :chain, *rest) # :nodoc: + if opts == :chain + WithChain.new(self) + else + self.with_values += [opts] + rest + self + end + end + + def build_arel + arel = super() + + build_with(arel) if @values[:with] + + arel + end + + def build_with(arel) + with_statements = with_values.flat_map do |with_value| + case with_value + when String + with_value + when Hash + with_value.map do |name, expression| + case expression + when String + select = Arel::Nodes::SqlLiteral.new "(#{expression})" + when ActiveRecord::Relation, Arel::SelectManager + select = Arel::Nodes::SqlLiteral.new "(#{expression.to_sql})" + end + Arel::Nodes::As.new Arel::Nodes::SqlLiteral.new("\"#{name}\""), select + end + when Arel::Nodes::As + with_value + end + end + + unless with_statements.empty? + if recursive_value + arel.with :recursive, with_statements + else + arel.with with_statements + end + end + end + end +end diff --git a/config/initializers/server_uptime.rb b/config/initializers/server_uptime.rb new file mode 100644 index 0000000000000000000000000000000000000000..46bf242e1430c258ef401a6fa231aa888d9a5865 --- /dev/null +++ b/config/initializers/server_uptime.rb @@ -0,0 +1 @@ +Rails.application.config.booted_at = Time.now diff --git a/config/routes.rb b/config/routes.rb index 2584981bb040e3448f462fe7516e9fd9b7d40301..846054e6917e17266b7e7d755246e0dd27b40ff0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,5 @@ require 'sidekiq/web' require 'sidekiq/cron/web' -require 'constraints/group_url_constrainer' Rails.application.routes.draw do concern :access_requestable do @@ -85,20 +84,6 @@ Rails.application.routes.draw do root to: "root#index" - # Since group show page is wildcard routing - # we want all other routing to be checked before matching this one - constraints(GroupUrlConstrainer.new) do - scope(path: '*id', - as: :group, - constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ }, - controller: :groups) do - get '/', action: :show - patch '/', action: :update - put '/', action: :update - delete '/', action: :destroy - end - end - draw :test if Rails.env.test? get '*unmatched_route', to: 'application#route_not_found' diff --git a/config/routes/admin.rb b/config/routes/admin.rb index b1b6ef33a47a3541e9264515141b8ecfd59ce2e6..c20581b133365fa5111e6c9f8226f9bf7b4b1ca8 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -36,7 +36,7 @@ namespace :admin do scope(path: 'groups/*id', controller: :groups, - constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ }) do + constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }) do scope(as: :group) do put :members_update @@ -54,6 +54,12 @@ namespace :admin do member do get :test end + + resources :hook_logs, only: [:show] do + member do + get :retry + end + end end resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do @@ -70,10 +76,10 @@ namespace :admin do scope(path: 'projects/*namespace_id', as: :namespace, - constraints: { namespace_id: Gitlab::Regex.namespace_route_regex }) do + constraints: { namespace_id: Gitlab::PathRegex.full_namespace_route_regex }) do resources(:projects, path: '/', - constraints: { id: Gitlab::Regex.project_route_regex }, + constraints: { id: Gitlab::PathRegex.project_route_regex }, only: [:show]) do member do diff --git a/config/routes/git_http.rb b/config/routes/git_http.rb index cdf658c3e4a41126d9b9e789b47072932ed3778d..a53c94326d4f3fab3a1427fc3f364455a0eecaf5 100644 --- a/config/routes/git_http.rb +++ b/config/routes/git_http.rb @@ -1,7 +1,7 @@ scope(path: '*namespace_id/:project_id', format: nil, - constraints: { namespace_id: Gitlab::Regex.namespace_route_regex }) do - scope(constraints: { project_id: Gitlab::Regex.project_git_route_regex }, module: :projects) do + constraints: { namespace_id: Gitlab::PathRegex.full_namespace_route_regex }) do + scope(constraints: { project_id: Gitlab::PathRegex.project_git_route_regex }, module: :projects) do # Git HTTP clients ('git clone' etc.) scope(controller: :git_http) do get '/info/refs', action: :info_refs @@ -28,7 +28,7 @@ scope(path: '*namespace_id/:project_id', end # Redirect /group/project/info/refs to /group/project.git/info/refs - scope(constraints: { project_id: Gitlab::Regex.project_route_regex }) do + scope(constraints: { project_id: Gitlab::PathRegex.project_route_regex }) do # Allow /info/refs, /info/refs?service=git-upload-pack, and # /info/refs?service=git-receive-pack, but nothing else. # diff --git a/config/routes/group.rb b/config/routes/group.rb index 7b29e0e807c9e38723bb34044d4c52e7c7958800..11cdff55ed89d9ddafb9bed5a0f7e4839527b2d3 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -1,9 +1,11 @@ +require 'constraints/group_url_constrainer' + resources :groups, only: [:index, :new, :create] scope(path: 'groups/*group_id', module: :groups, as: :group, - constraints: { group_id: Gitlab::Regex.namespace_route_regex }) do + constraints: { group_id: Gitlab::PathRegex.full_namespace_route_regex }) do resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do post :resend_invite, on: :member delete :leave, on: :collection @@ -25,7 +27,7 @@ end scope(path: 'groups/*id', controller: :groups, - constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ }) do + constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }) do get :edit, as: :edit_group get :issues, as: :issues_group get :merge_requests, as: :merge_requests_group @@ -34,3 +36,15 @@ scope(path: 'groups/*id', get :subgroups, as: :subgroups_group get '/', action: :show, as: :group_canonical end + +constraints(GroupUrlConstrainer.new) do + scope(path: '*id', + as: :group, + constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }, + controller: :groups) do + get '/', action: :show + patch '/', action: :update + put '/', action: :update + delete '/', action: :destroy + end +end diff --git a/config/routes/project.rb b/config/routes/project.rb index 89b14ef6527fc85736be5d31c4c1a064e8b812ed..6922f8db795f4e6d4085b679b491cc21fe365383 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -13,16 +13,16 @@ constraints(ProjectUrlConstrainer.new) do # Otherwise, Rails will overwrite the constraint with `/.+?/`, # which breaks some of our wildcard routes like `/blob/*id` # and `/tree/*id` that depend on the negative lookahead inside - # `Gitlab::Regex.namespace_route_regex`, which helps the router + # `Gitlab::PathRegex.full_namespace_route_regex`, which helps the router # determine whether a certain path segment is part of `*namespace_id`, # `:project_id`, or `*id`. # # See https://github.com/rails/rails/blob/v4.2.8/actionpack/lib/action_dispatch/routing/mapper.rb#L155 scope(path: '*namespace_id', as: :namespace, - namespace_id: Gitlab::Regex.namespace_route_regex) do + namespace_id: Gitlab::PathRegex.full_namespace_route_regex) do scope(path: ':project_id', - constraints: { project_id: Gitlab::Regex.project_route_regex }, + constraints: { project_id: Gitlab::PathRegex.project_route_regex }, module: :projects, as: :project) do @@ -222,6 +222,12 @@ constraints(ProjectUrlConstrainer.new) do member do get :test end + + resources :hook_logs, only: [:show] do + member do + get :retry + end + end end resources :container_registry, only: [:index, :destroy], @@ -335,7 +341,7 @@ constraints(ProjectUrlConstrainer.new) do resources :runner_projects, only: [:create, :destroy] resources :badges, only: [:index] do collection do - scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do + scope '*ref', constraints: { ref: Gitlab::PathRegex.git_reference_regex } do constraints format: /svg/ do get :build get :coverage @@ -358,7 +364,7 @@ constraints(ProjectUrlConstrainer.new) do resources(:projects, path: '/', - constraints: { id: Gitlab::Regex.project_route_regex }, + constraints: { id: Gitlab::PathRegex.project_route_regex }, only: [:edit, :show, :update, :destroy]) do member do put :transfer diff --git a/config/routes/repository.rb b/config/routes/repository.rb index 5cf37a06e97c0eab5e8d557b65ae7e1826388c82..11911636fa77588f9bc29a9fe50961783d4ecb3c 100644 --- a/config/routes/repository.rb +++ b/config/routes/repository.rb @@ -2,7 +2,7 @@ resource :repository, only: [:create] do member do - get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex } + get 'archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex } end end @@ -24,7 +24,7 @@ scope format: false do member do # tree viewer logs - get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex } + get 'logs_tree', constraints: { id: Gitlab::PathRegex.git_reference_regex } # Directories with leading dots erroneously get rejected if git # ref regex used in constraints. Regex verification now done in controller. get 'logs_tree/*path', action: :logs_tree, as: :logs_file, format: false, constraints: { @@ -34,7 +34,7 @@ scope format: false do end end - scope constraints: { id: Gitlab::Regex.git_reference_regex } do + scope constraints: { id: Gitlab::PathRegex.git_reference_regex } do resources :network, only: [:show] resources :graphs, only: [:show] do diff --git a/config/routes/user.rb b/config/routes/user.rb index 0f3bec9cf5877b61ef32f1f85e4e78c4bdd6527f..e682dcd666352a412a9b2021ee32378d627e2fd6 100644 --- a/config/routes/user.rb +++ b/config/routes/user.rb @@ -11,19 +11,7 @@ devise_scope :user do get '/users/almost_there' => 'confirmations#almost_there' end -constraints(UserUrlConstrainer.new) do - # Get all keys of user - get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: Gitlab::Regex.root_namespace_route_regex } - - scope(path: ':username', - as: :user, - constraints: { username: Gitlab::Regex.root_namespace_route_regex }, - controller: :users) do - get '/', action: :show - end -end - -scope(constraints: { username: Gitlab::Regex.root_namespace_route_regex }) do +scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) do scope(path: 'users/:username', as: :user, controller: :users) do @@ -34,7 +22,7 @@ scope(constraints: { username: Gitlab::Regex.root_namespace_route_regex }) do get :contributed, as: :contributed_projects get :snippets get :exists - get '/', to: redirect('/%{username}') + get '/', to: redirect('/%{username}'), as: nil end # Compatibility with old routing @@ -46,3 +34,15 @@ scope(constraints: { username: Gitlab::Regex.root_namespace_route_regex }) do get '/u/:username/snippets', to: redirect('/users/%{username}/snippets') get '/u/:username/contributed', to: redirect('/users/%{username}/contributed') end + +constraints(UserUrlConstrainer.new) do + # Get all keys of user + get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: Gitlab::PathRegex.root_namespace_route_regex } + + scope(path: ':username', + as: :user, + constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }, + controller: :users) do + get '/', action: :show + end +end diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 0ca1f56518519e9010d42973b60a727193f6d038..93df2d6f5ff549c9f40720a5b6906672d6f431b6 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -44,9 +44,8 @@ - [project_cache, 1] - [project_destroy, 1] - [project_export, 1] - - [project_web_hook, 1] + - [web_hook, 1] - [repository_check, 1] - - [system_hook, 1] - [git_garbage_collect, 1] - [reactive_caching, 1] - [cronjob, 1] diff --git a/config/webpack.config.js b/config/webpack.config.js index 42024739fe938257c1e872bea4e07c018a630be1..c77b1d6334cfbdeea72ad81803cc81402aa2df22 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -5,6 +5,7 @@ var path = require('path'); var webpack = require('webpack'); var StatsPlugin = require('stats-webpack-plugin'); var CompressionPlugin = require('compression-webpack-plugin'); +var NameAllModulesPlugin = require('name-all-modules-plugin'); var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); @@ -23,6 +24,7 @@ var config = { }, context: path.join(ROOT_PATH, 'app/assets/javascripts'), entry: { + balsamiq_viewer: './blob/balsamiq_viewer.js', blob: './blob_edit/blob_bundle.js', boards: './boards/boards_bundle.js', common: './commons/index.js', @@ -47,8 +49,7 @@ var config = { notebook_viewer: './blob/notebook_viewer.js', pdf_viewer: './blob/pdf_viewer.js', pipelines: './pipelines/index.js', - balsamiq_viewer: './blob/balsamiq_viewer.js', - pipelines_graph: './pipelines/graph_bundle.js', + pipelines_details: './pipelines/pipeline_details_bundle.js', profile: './profile/profile_bundle.js', protected_branches: './protected_branches/protected_branches_bundle.js', protected_tags: './protected_tags', @@ -69,7 +70,8 @@ var config = { output: { path: path.join(ROOT_PATH, 'public/assets/webpack'), publicPath: '/assets/webpack/', - filename: IS_PRODUCTION ? '[name].[chunkhash].bundle.js' : '[name].bundle.js' + filename: IS_PRODUCTION ? '[name].[chunkhash].bundle.js' : '[name].bundle.js', + chunkFilename: IS_PRODUCTION ? '[name].[chunkhash].chunk.js' : '[name].chunk.js', }, devtool: 'cheap-module-source-map', @@ -100,7 +102,7 @@ var config = { loader: 'file-loader', }, { - test: /locale\/[a-z]+\/(.*)\.js$/, + test: /locale\/\w+\/(.*)\.js$/, loader: 'exports-loader?locales', }, ] @@ -126,8 +128,20 @@ var config = { jQuery: 'jquery', }), - // use deterministic module ids + // assign deterministic module ids new webpack.NamedModulesPlugin(), + new NameAllModulesPlugin(), + + // assign deterministic chunk ids + new webpack.NamedChunksPlugin((chunk) => { + if (chunk.name) { + return chunk.name; + } + return chunk.modules.map((m) => { + var chunkPath = m.request.split('!').pop(); + return path.relative(m.context, chunkPath); + }).join('_'); + }), // create cacheable common library bundle for all vue chunks new webpack.optimize.CommonsChunkPlugin({ @@ -146,7 +160,7 @@ var config = { 'notebook_viewer', 'pdf_viewer', 'pipelines', - 'pipelines_graph', + 'pipelines_details', 'schedule_form', 'schedules_index', 'sidebar', diff --git a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb index bd0463886bc0a46bae6d07837cbe048f3f9d40e8..4d6a61bd6142404ae8912a20aabaa02327c94160 100644 --- a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb +++ b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateColumnInBatches class SetMissingStageOnCiBuilds < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb index 1eb99feb40c9ef3d07ba11f69cfe16f384d653fe..b2a2ce41391092ccb5e6b036d8bf68a1e90c6334 100644 --- a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb +++ b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateColumnInBatches class DropAndReaddHasExternalWikiInProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb b/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb index f1a1f001cb303d7908cf99fc40bbbcdfca5f1b9c..febd2c0e65ebce90e7967f8abea75bb355c0ffec 100644 --- a/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb +++ b/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateColumnInBatches class SetConfidentialIssuesEventsOnWebhooks < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160919144305_add_type_to_labels.rb b/db/migrate/20160919144305_add_type_to_labels.rb index 66172bda6ffc92f967ea0f0d52289a60916bf754..2d2725ccf5900d7af2fb4a2e431e2290660ac72b 100644 --- a/db/migrate/20160919144305_add_type_to_labels.rb +++ b/db/migrate/20160919144305_add_type_to_labels.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateColumnInBatches class AddTypeToLabels < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161018124658_make_project_owners_masters.rb b/db/migrate/20161018124658_make_project_owners_masters.rb index a576bb7b6222920b939b928a543835b7099a63b8..fe11699c196ff0d4010a5d7387f1737a9f672069 100644 --- a/db/migrate/20161018124658_make_project_owners_masters.rb +++ b/db/migrate/20161018124658_make_project_owners_masters.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateColumnInBatches class MakeProjectOwnersMasters < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb b/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb index 50ad74372274219f2f4f9861c0266d3eff343e46..c7cada6dfc523f9f997e11e1b89d297d3a2cff82 100644 --- a/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb +++ b/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateColumnInBatches class RenameSlackAndMattermostNotificationServices < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170317203554_index_routes_path_for_like.rb b/db/migrate/20170317203554_index_routes_path_for_like.rb index 7ac09b2abe5bed30b1f6b8c9c6605f0553a1f949..8d3609135d0f475282faad7f43d5e42ef7e42c00 100644 --- a/db/migrate/20170317203554_index_routes_path_for_like.rb +++ b/db/migrate/20170317203554_index_routes_path_for_like.rb @@ -21,9 +21,8 @@ class IndexRoutesPathForLike < ActiveRecord::Migration def down return unless Gitlab::Database.postgresql? + return unless index_exists?(:routes, :path, name: INDEX_NAME) - if index_exists?(:routes, :path, name: INDEX_NAME) - execute("DROP INDEX CONCURRENTLY #{INDEX_NAME};") - end + remove_concurrent_index_by_name(:routes, INDEX_NAME) end end diff --git a/db/migrate/20170320173259_migrate_assignees.rb b/db/migrate/20170320173259_migrate_assignees.rb index 23e7500a32d808429144ee4165fc3d471a9c52ef..7b61e811317b51ebe8c97c7f715ba2b10318910b 100644 --- a/db/migrate/20170320173259_migrate_assignees.rb +++ b/db/migrate/20170320173259_migrate_assignees.rb @@ -1,6 +1,4 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - +# rubocop:disable Migration/UpdateColumnInBatches class MigrateAssignees < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb b/db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb index 9d4380ef96082f3a5f8fac583bac1f90778f420b..84635fa39b9c2921c3efa8dae5fccad76b3be3b3 100644 --- a/db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb +++ b/db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb @@ -11,13 +11,7 @@ class RemoveIndexForUsersCurrentSignInAt < ActiveRecord::Migration disable_ddl_transaction! def up - if index_exists? :users, :current_sign_in_at - if Gitlab::Database.postgresql? - execute 'DROP INDEX CONCURRENTLY index_users_on_current_sign_in_at;' - else - remove_concurrent_index :users, :current_sign_in_at - end - end + remove_concurrent_index :users, :current_sign_in_at end def down diff --git a/db/migrate/20170427103502_create_web_hook_logs.rb b/db/migrate/20170427103502_create_web_hook_logs.rb new file mode 100644 index 0000000000000000000000000000000000000000..3643c52180c90c766dae4166d4b925ba80ea72db --- /dev/null +++ b/db/migrate/20170427103502_create_web_hook_logs.rb @@ -0,0 +1,22 @@ +# rubocop:disable all +class CreateWebHookLogs < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :web_hook_logs do |t| + t.references :web_hook, null: false, index: true, foreign_key: { on_delete: :cascade } + + t.string :trigger + t.string :url + t.text :request_headers + t.text :request_data + t.text :response_headers + t.text :response_body + t.string :response_status + t.float :execution_duration + t.string :internal_error_message + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20170503140201_reschedule_project_authorizations.rb b/db/migrate/20170503140201_reschedule_project_authorizations.rb new file mode 100644 index 0000000000000000000000000000000000000000..fa45adadbae80f3f21760e75e123b39952833448 --- /dev/null +++ b/db/migrate/20170503140201_reschedule_project_authorizations.rb @@ -0,0 +1,44 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RescheduleProjectAuthorizations < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + class User < ActiveRecord::Base + self.table_name = 'users' + end + + def up + offset = 0 + batch = 5000 + start = Time.now + + loop do + relation = User.where('id > ?', offset) + user_ids = relation.limit(batch).reorder(id: :asc).pluck(:id) + + break if user_ids.empty? + + offset = user_ids.last + + # This will schedule each batch 5 minutes after the previous batch was + # scheduled. This smears out the load over time, instead of immediately + # scheduling a million jobs. + Sidekiq::Client.push_bulk( + 'queue' => 'authorized_projects', + 'args' => user_ids.zip, + 'class' => 'AuthorizedProjectsWorker', + 'at' => start.to_i + ) + + start += 5.minutes + end + end + + def down + end +end diff --git a/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb b/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb new file mode 100644 index 0000000000000000000000000000000000000000..c67690642c98b2a450c5e6405f4f3c7e32ab84a3 --- /dev/null +++ b/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb @@ -0,0 +1,123 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +# This migration depends on code external to it. For example, it relies on +# updating a namespace to also rename directories (uploads, GitLab pages, etc). +# The alternative is to copy hundreds of lines of code into this migration, +# adjust them where needed, etc; something which doesn't work well at all. +class TurnNestedGroupsIntoRegularGroupsForMysql < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def run_migration? + Gitlab::Database.mysql? + end + + def up + return unless run_migration? + + # For all sub-groups we need to give the right people access. We do this as + # follows: + # + # 1. Get all the ancestors for the current namespace + # 2. Get all the members of these namespaces, along with their higher access + # level + # 3. Give these members access to the current namespace + Namespace.unscoped.where('parent_id IS NOT NULL').find_each do |namespace| + rows = [] + existing = namespace.members.pluck(:user_id) + + all_members_for(namespace).each do |member| + next if existing.include?(member[:user_id]) + + rows << { + access_level: member[:access_level], + source_id: namespace.id, + source_type: 'Namespace', + user_id: member[:user_id], + notification_level: 3, # global + type: 'GroupMember', + created_at: Time.current, + updated_at: Time.current + } + end + + bulk_insert_members(rows) + + # This method relies on the parent to determine the proper path. + # Because we reset "parent_id" this method will not return the right path + # when moving namespaces. + full_path_was = namespace.send(:full_path_was) + + namespace.define_singleton_method(:full_path_was) { full_path_was } + + namespace.update!(parent_id: nil, path: new_path_for(namespace)) + end + end + + def down + # There is no way to go back from regular groups to nested groups. + end + + # Generates a new (unique) path for a namespace. + def new_path_for(namespace) + counter = 1 + base = namespace.full_path.tr('/', '-') + new_path = base + + while Namespace.unscoped.where(path: new_path).exists? + new_path = base + "-#{counter}" + counter += 1 + end + + new_path + end + + # Returns an Array containing all the ancestors of the current namespace. + # + # This method is not particularly efficient, but it's probably still faster + # than using the "routes" table. Most importantly of all, it _only_ depends + # on the namespaces table and the "parent_id" column. + def ancestors_for(namespace) + ancestors = [] + current = namespace + + while current&.parent_id + # We're using find_by(id: ...) here to deal with cases where the + # parent_id may point to a missing row. + current = Namespace.unscoped.select([:id, :parent_id]). + find_by(id: current.parent_id) + + ancestors << current.id if current + end + + ancestors + end + + # Returns a relation containing all the members that have access to any of + # the current namespace's parent namespaces. + def all_members_for(namespace) + Member. + unscoped. + select(['user_id', 'MAX(access_level) AS access_level']). + where(source_type: 'Namespace', source_id: ancestors_for(namespace)). + group(:user_id) + end + + def bulk_insert_members(rows) + return if rows.empty? + + keys = rows.first.keys + + tuples = rows.map do |row| + row.map { |(_, value)| connection.quote(value) } + end + + execute <<-EOF.strip_heredoc + INSERT INTO members (#{keys.join(', ')}) + VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')} + EOF + end +end diff --git a/db/migrate/20170503185032_index_redirect_routes_path_for_like.rb b/db/migrate/20170503185032_index_redirect_routes_path_for_like.rb index 5b8b6c828be8ee08375310165f0e3acae0b3a689..8eb20faa03af5c6017ca4d3a06c77353f47036ce 100644 --- a/db/migrate/20170503185032_index_redirect_routes_path_for_like.rb +++ b/db/migrate/20170503185032_index_redirect_routes_path_for_like.rb @@ -21,9 +21,8 @@ class IndexRedirectRoutesPathForLike < ActiveRecord::Migration def down return unless Gitlab::Database.postgresql? + return unless index_exists?(:redirect_routes, :path, name: INDEX_NAME) - if index_exists?(:redirect_routes, :path, name: INDEX_NAME) - execute("DROP INDEX CONCURRENTLY #{INDEX_NAME};") - end + remove_concurrent_index_by_name(:redirect_routes, INDEX_NAME) end end diff --git a/db/migrate/20170504182103_add_index_project_group_links_group_id.rb b/db/migrate/20170504182103_add_index_project_group_links_group_id.rb new file mode 100644 index 0000000000000000000000000000000000000000..62bf641daa6ab4719fe7ea08850f7e03076c0527 --- /dev/null +++ b/db/migrate/20170504182103_add_index_project_group_links_group_id.rb @@ -0,0 +1,19 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndexProjectGroupLinksGroupId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :project_group_links, :group_id + end + + def down + remove_concurrent_index :project_group_links, :group_id + end +end diff --git a/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb index b518038e93a9c6c381decaeaf913c65f0535427b..82f8147547ebb465e4568011f9f53e4c2a81084e 100644 --- a/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb +++ b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb @@ -1,6 +1,4 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - +# rubocop:disable Migration/UpdateColumnInBatches class ResetUsersAuthorizedProjectsPopulated < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb index b61dd7cfc618d528a59b4b2b277cbab581a483cf..b1c9eed114825cfb942fa940e05157fa1977223d 100644 --- a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb +++ b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb @@ -1,6 +1,4 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - +# rubocop:disable Migration/UpdateColumnInBatches class ResetRelativePositionForIssue < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb index a19b73fc114b6cb94aec7b9b8422a09e0446a0ba..3c13a3d2518b6a43178bc6a7bdfe5e4c565bb23d 100644 --- a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb +++ b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateColumnInBatches class EnableAutoCancelPendingPipelinesForAll < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170503120310_remove_users_authorized_projects_populated.rb b/db/post_migrate/20170503120310_remove_users_authorized_projects_populated.rb new file mode 100644 index 0000000000000000000000000000000000000000..1b44334395f90248cab48e55184534930a55960b --- /dev/null +++ b/db/post_migrate/20170503120310_remove_users_authorized_projects_populated.rb @@ -0,0 +1,15 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveUsersAuthorizedProjectsPopulated < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def change + remove_column :users, :authorized_projects_populated, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index ca33c8cc2a2b9076b18bf1631268f37167ff8fd2..4c73f74ef1f9654954ce65c1fa0fb2ed1c7501c7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -928,6 +928,8 @@ ActiveRecord::Schema.define(version: 20170523091700) do t.date "expires_at" end + add_index "project_group_links", ["group_id"], name: "index_project_group_links_on_group_id", using: :btree + create_table "project_import_data", force: :cascade do |t| t.integer "project_id" t.text "data" @@ -1355,7 +1357,6 @@ ActiveRecord::Schema.define(version: 20170523091700) do t.boolean "external", default: false t.string "incoming_email_token" t.string "organization" - t.boolean "authorized_projects_populated" t.boolean "require_two_factor_authentication_from_group", default: false, null: false t.integer "two_factor_grace_period", default: 48, null: false t.boolean "ghost" @@ -1391,6 +1392,23 @@ ActiveRecord::Schema.define(version: 20170523091700) do add_index "users_star_projects", ["project_id"], name: "index_users_star_projects_on_project_id", using: :btree add_index "users_star_projects", ["user_id", "project_id"], name: "index_users_star_projects_on_user_id_and_project_id", unique: true, using: :btree + create_table "web_hook_logs", force: :cascade do |t| + t.integer "web_hook_id", null: false + t.string "trigger" + t.string "url" + t.text "request_headers" + t.text "request_data" + t.text "response_headers" + t.text "response_body" + t.string "response_status" + t.float "execution_duration" + t.string "internal_error_message" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "web_hook_logs", ["web_hook_id"], name: "index_web_hook_logs_on_web_hook_id", using: :btree + create_table "web_hooks", force: :cascade do |t| t.string "url", limit: 2000 t.integer "project_id" @@ -1454,4 +1472,5 @@ ActiveRecord::Schema.define(version: 20170523091700) do add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade add_foreign_key "trending_projects", "projects", on_delete: :cascade add_foreign_key "u2f_registrations", "users" + add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade end diff --git a/doc/development/i18n_guide.md b/doc/development/i18n_guide.md index 44eca68aacaad5d654ff8dbd4deb14107d0f1e9c..735345bd1266579bf9f665ecf6c13843798e8bcd 100644 --- a/doc/development/i18n_guide.md +++ b/doc/development/i18n_guide.md @@ -7,6 +7,14 @@ For working with internationalization (i18n) we use tool for this task and we have a lot of applications that will help us to work with it. +## Setting up GitLab Development Kit (GDK) + +In order to be able to work on the [GitLab Community Edition](https://gitlab.com/gitlab-org/gitlab-ce) project we must download and +configure it through [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit), we can do it by following this [guide](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/set-up-gdk.md). + +Once we have the GitLab project ready we can start working on the +translation of the project. + ## Tools We use a couple of gems: @@ -211,9 +219,11 @@ Let's suppose you want to add translations for a new language, let's say French. you just need to separate the region with an underscore (`_`). For example: ```sh - bundle exec rake gettext:add_language[en_gb] + bundle exec rake gettext:add_language[en_GB] ``` + Please note that you need to specify the region part in capitals. + 1. Now that the language is added, a new directory has been created under the path: `locale/fr/`. You can now start using your PO editor to edit the PO file located in: `locale/fr/gitlab.edit.po`. diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md index da2dac23c6a12d65d3e2143ebb87e3ab9875aeb2..9a171d346714d57a5b84f2a66d6e555ee063e54d 100644 --- a/doc/install/database_mysql.md +++ b/doc/install/database_mysql.md @@ -281,5 +281,5 @@ GitLab database to `longtext` columns, which can persist values of up to 4GB Details can be found in the [PostgreSQL][postgres-text-type] and [MySQL][mysql-text-types] manuals. -[postgres-text-type]: http://www.postgresql.org/docs/9.1/static/datatype-character.html +[postgres-text-type]: http://www.postgresql.org/docs/9.2/static/datatype-character.html [mysql-text-types]: http://dev.mysql.com/doc/refman/5.7/en/string-type-overview.html diff --git a/doc/install/installation.md b/doc/install/installation.md index 5bba405f159cb0f83515da2525109cf4bff316df..cda70b78c6156819f0fe2b91eca829a003ab3700 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -185,7 +185,8 @@ Create a `git` user for GitLab: We recommend using a PostgreSQL database. For MySQL check the [MySQL setup guide](database_mysql.md). -> **Note**: because we need to make use of extensions you need at least pgsql 9.1. +> **Note**: because we need to make use of extensions and concurrent index removal, +you need at least PostgreSQL 9.2. 1. Install the database packages: diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 2e456557d776bc0e824def7f6961a14f9f776e7d..5338ccb9d3a4c5b702e0011917dd42378526bb42 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -138,7 +138,7 @@ installation (e.g. the number of users, projects, etc). ### PostgreSQL Requirements -As of GitLab 9.0, PostgreSQL 9.2 or newer is required, and earlier versions are +As of GitLab 9.3, PostgreSQL 9.2 or newer is required, and earlier versions are not supported. We highly recommend users to use at least PostgreSQL 9.6 as this is the PostgreSQL version used for development and testing. diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 151c17f3bf18567c309618f66377a0e9312f6887..c4921c74a17bcc9eb2c71ce87f406b0996104abc 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -13,6 +13,15 @@ up to 20 levels of nested groups, which among other things can help you to: - **Make it easier to manage people and control visibility.** Give people different [permissions][] depending on their group [membership](#membership). +## Database Requirements + +Nested groups are only supported when you use PostgreSQL. Supporting nested +groups on MySQL in an efficient way is not possible due to MySQL's limitations. +See the following links for more information: + +* <https://gitlab.com/gitlab-org/gitlab-ce/issues/30472> +* <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10885> + ## Overview A group can have many subgroups inside it, and at the same time a group can have @@ -71,7 +80,7 @@ structure. - You need to be an Owner of a group in order to be able to create a subgroup. For more information check the [permissions table][permissions]. - For a list of words that are not allowed to be used as group names see the - [`regex.rb` file][reserved] under the `TOP_LEVEL_ROUTES`, `PROJECT_WILDCARD_ROUTES` and `GROUP_ROUTES` lists: + [`path_regex.rb` file][reserved] under the `TOP_LEVEL_ROUTES`, `PROJECT_WILDCARD_ROUTES` and `GROUP_ROUTES` lists: - `TOP_LEVEL_ROUTES`: are names that are reserved as usernames or top level groups - `PROJECT_WILDCARD_ROUTES`: are names that are reserved for child groups or projects. - `GROUP_ROUTES`: are names that are reserved for all groups or projects. @@ -163,4 +172,4 @@ Here's a list of what you can't do with subgroups: [ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772 [permissions]: ../../permissions.md#group -[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/regex.rb +[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/path_regex.rb diff --git a/doc/user/project/integrations/img/webhook_logs.png b/doc/user/project/integrations/img/webhook_logs.png new file mode 100755 index 0000000000000000000000000000000000000000..917068d9398b807a1ab367d52755c819233f0a9e Binary files /dev/null and b/doc/user/project/integrations/img/webhook_logs.png differ diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index f611029afdc24582e4aa3ed6bc2d84401e413ebe..a048260b033ad3d0da7a068234badf3d2f11f199 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -97,7 +97,8 @@ in the table below. | Field | Description | | ----- | ----------- | -| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. E.g., `https://jira.example.com`. | +| `Web URL` | The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. | +| `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. | | `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | | `Username` | The user name created in [configuring JIRA step](#configuring-jira). | | `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 48d49c5d40cea293c1715e568d4e2df0ef1bf3b8..d0bb1cd11a862428389e4801bca834de6f7e131f 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -1017,6 +1017,22 @@ X-Gitlab-Event: Build Hook } ``` +## Troubleshoot webhooks + +Gitlab stores each perform of the webhook. +You can find records for last 2 days in "Recent Deliveries" section on the edit page of each webhook. + + + +In this section you can see HTTP status code (green for 200-299 codes, red for the others, `internal error` for failed deliveries ), triggered event, a time when the event was called, elapsed time of the request. + +If you need more information about execution, you can click `View details` link. +On this page, you can see data that GitLab sends (request headers and body) and data that it received (response headers and body). + +From this page, you can repeat delivery with the same data by clicking `Resend Request` button. + +>**Note:** If URL or secret token of the webhook were updated, data will be delivered to the new address. + ## Example webhook receiver If you want to see GitLab's webhooks in action for testing purposes you can use diff --git a/features/dashboard/starred_projects.feature b/features/dashboard/starred_projects.feature deleted file mode 100644 index 9dfd2fbab9cb2a43e3364f25ccd22b5185259d20..0000000000000000000000000000000000000000 --- a/features/dashboard/starred_projects.feature +++ /dev/null @@ -1,12 +0,0 @@ -@dashboard -Feature: Dashboard Starred Projects - Background: - Given I sign in as a user - And public project "Community" - And I starred project "Community" - And I own project "Shop" - And I visit dashboard starred projects page - - Scenario: I should see projects list - Then I should see project "Community" - And I should not see project "Shop" diff --git a/features/project/hooks.feature b/features/project/hooks.feature deleted file mode 100644 index 627738004c4afcbd2df16758fc5ba32094c4efaf..0000000000000000000000000000000000000000 --- a/features/project/hooks.feature +++ /dev/null @@ -1,37 +0,0 @@ -Feature: Project Hooks - Background: - Given I sign in as a user - And I own project "Shop" - - Scenario: I should see hook list - Given project has hook - When I visit project hooks page - Then I should see project hook - - Scenario: I add new hook - Given I visit project hooks page - When I submit new hook - Then I should see newly created hook - - Scenario: I add new hook with SSL verification enabled - Given I visit project hooks page - When I submit new hook with SSL verification enabled - Then I should see newly created hook with SSL verification enabled - - Scenario: I test hook - Given project has hook - And I visit project hooks page - When I click test hook button - Then hook should be triggered - - Scenario: I test a hook on empty project - Given I own empty project with hook - And I visit project hooks page - When I click test hook button - Then I should see hook error message - - Scenario: I test a hook on down URL - Given project has hook - And I visit project hooks page - When I click test hook button with invalid URL - Then I should see hook service down error message diff --git a/features/project/merge_requests/accept.feature b/features/project/merge_requests/accept.feature index c45ed9ea68b3d378a1d1a8deafce7b62da5d5a73..2ab1c19f4527b82c96611b6c19f32a3a8d5199fa 100644 --- a/features/project/merge_requests/accept.feature +++ b/features/project/merge_requests/accept.feature @@ -7,6 +7,7 @@ Feature: Project Merge Requests Acceptance @javascript Scenario: Accepting the Merge Request and removing the source branch Given I am on the Merge Request detail page + When I check the "Remove source branch" option And I click on Accept Merge Request Then I should see merge request merged And I should not see the Remove Source Branch button @@ -14,6 +15,7 @@ Feature: Project Merge Requests Acceptance @javascript Scenario: Accepting the Merge Request when URL has an anchor Given I am on the Merge Request detail with note anchor page + When I check the "Remove source branch" option And I click on Accept Merge Request Then I should see merge request merged And I should not see the Remove Source Branch button @@ -21,7 +23,6 @@ Feature: Project Merge Requests Acceptance @javascript Scenario: Accepting the Merge Request without removing the source branch Given I am on the Merge Request detail page - When I click on "Remove source branch" option When I click on Accept Merge Request Then I should see merge request merged And I should see the Remove Source Branch button diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index bf09d7b7114be86caba6d64515273a92350637e3..71c69a4fdea79cd4b3ce1a4a8390ab0b2a8e8c70 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -22,7 +22,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps end step 'I click "Create merge request" link' do - click_link "Create merge request" + find_link("Create merge request", visible: false).trigger('click') end step 'I see prefilled new Merge Request page' do diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb deleted file mode 100644 index 945d58a64581a6742a0ac3caad35c99b8e6fb817..0000000000000000000000000000000000000000 --- a/features/steps/project/hooks.rb +++ /dev/null @@ -1,75 +0,0 @@ -require 'webmock' - -class Spinach::Features::ProjectHooks < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - include RSpec::Matchers - include RSpec::Mocks::ExampleMethods - include WebMock::API - - step 'project has hook' do - @hook = create(:project_hook, project: current_project) - end - - step 'I own empty project with hook' do - @project = create(:empty_project, - name: 'Empty Project', namespace: @user.namespace) - @hook = create(:project_hook, project: current_project) - end - - step 'I should see project hook' do - expect(page).to have_content @hook.url - end - - step 'I submit new hook' do - @url = 'http://example.org/1' - fill_in "hook_url", with: @url - expect { click_button "Add webhook" }.to change(ProjectHook, :count).by(1) - end - - step 'I submit new hook with SSL verification enabled' do - @url = 'http://example.org/2' - fill_in "hook_url", with: @url - check "hook_enable_ssl_verification" - expect { click_button "Add webhook" }.to change(ProjectHook, :count).by(1) - end - - step 'I should see newly created hook' do - expect(current_path).to eq namespace_project_settings_integrations_path(current_project.namespace, current_project) - expect(page).to have_content(@url) - end - - step 'I should see newly created hook with SSL verification enabled' do - expect(current_path).to eq namespace_project_settings_integrations_path(current_project.namespace, current_project) - expect(page).to have_content(@url) - expect(page).to have_content("SSL Verification: enabled") - end - - step 'I click test hook button' do - stub_request(:post, @hook.url).to_return(status: 200) - click_link 'Test' - end - - step 'I click test hook button with invalid URL' do - stub_request(:post, @hook.url).to_raise(SocketError) - click_link 'Test' - end - - step 'hook should be triggered' do - expect(current_path).to eq namespace_project_settings_integrations_path(current_project.namespace, current_project) - expect(page).to have_selector '.flash-notice', - text: 'Hook executed successfully: HTTP 200' - end - - step 'I should see hook error message' do - expect(page).to have_selector '.flash-alert', - text: 'Hook execution failed. '\ - 'Ensure the project has commits.' - end - - step 'I should see hook service down error message' do - expect(page).to have_selector '.flash-alert', - text: 'Hook execution failed: Exception from' - end -end diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb index 023f9bef8e570fb969d0cd15362c8b0109d42913..870dc862992a9fc5bf8dccac24619bd1309fe385 100644 --- a/features/steps/project/merge_requests/acceptance.rb +++ b/features/steps/project/merge_requests/acceptance.rb @@ -11,10 +11,14 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps visit merge_request_path(@merge_request, anchor: 'note_123') end - step 'I click on "Remove source branch" option' do + step 'I uncheck the "Remove source branch" option' do uncheck('Remove source branch') end + step 'I check the "Remove source branch" option' do + check('Remove source branch') + end + step 'I click on Accept Merge Request' do click_button('Merge') end diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 3c0d987e403b81a26db59349d8d969097d30d9fa..66368a159ec9cd81ac6ccc0f9de5100b0795e654 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -178,7 +178,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I fill jira settings' do - fill_in 'URL', with: 'http://jira.example' + fill_in 'Web URL', with: 'http://jira.example' + fill_in 'JIRA API URL', with: 'http://jira.example/api' fill_in 'Username', with: 'gitlab' fill_in 'Password', with: 'gitlab' fill_in 'Project Key', with: 'GITLAB' @@ -186,7 +187,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see jira service settings saved' do - expect(find_field('URL').value).to eq 'http://jira.example' + expect(find_field('Web URL').value).to eq 'http://jira.example' + expect(find_field('JIRA API URL').value).to eq 'http://jira.example/api' expect(find_field('Username').value).to eq 'gitlab' expect(find_field('Project Key').value).to eq 'GITLAB' end diff --git a/lib/api/api.rb b/lib/api/api.rb index 52cd7cbe3dba345cf921d9213f52c354f7f9893c..ac113c5200d47cf60c675ea3f99767a177115328 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -45,9 +45,9 @@ module API end before { allow_access_with_scope :api } - before { Gitlab::I18n.set_locale(current_user) } + before { Gitlab::I18n.locale = current_user&.preferred_language } - after { Gitlab::I18n.reset_locale } + after { Gitlab::I18n.use_default_locale } rescue_from Gitlab::Access::AccessDeniedError do rack_response({ 'message' => '403 Forbidden' }.to_json, 403) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 01cc8e8e1cad5196f847d42bea022f120d02c176..8c5e5c917694ebf13bed5406eefa4b10f6772fe3 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -152,7 +152,10 @@ module API expose :web_url expose :request_access_enabled expose :full_name, :full_path - expose :parent_id + + if ::Group.supports_nested_groups? + expose :parent_id + end expose :statistics, if: :statistics do with_options format_with: -> (value) { value.to_i } do @@ -252,7 +255,9 @@ module API class RepoDiff < Grape::Entity expose :old_path, :new_path, :a_mode, :b_mode, :diff - expose :new_file, :renamed_file, :deleted_file + expose :new_file?, as: :new_file + expose :renamed_file?, as: :renamed_file + expose :deleted_file?, as: :deleted_file end class Milestone < ProjectEntity diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 3da7d735da88208d526686ebf9f11ad2a108e3c4..ee85b777afff7ef2b5869001fb65ad0469696d57 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -70,7 +70,11 @@ module API params do requires :name, type: String, desc: 'The name of the group' requires :path, type: String, desc: 'The path of the group' - optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group' + + if ::Group.supports_nested_groups? + optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group' + end + use :optional_params end post do diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 8f16e532ecbc18f799af07b3c4604e61448b7187..14d2bff9cb5cba3996ab93be7419b01310a302d5 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -85,7 +85,7 @@ module API optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded' optional :format, type: String, desc: 'The archive format' end - get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do + get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do begin send_git_archive user_project.repository, ref: params[:sha], format: params[:format] rescue diff --git a/lib/api/services.rb b/lib/api/services.rb index cb07df9e24972a8bd87cb7189ae6c78fb3dc9292..47bd9940f77181368bffcff147b23f7fdb96f1da 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -304,7 +304,13 @@ module API required: true, name: :url, type: String, - desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com' + desc: 'The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., https://jira.example.com' + }, + { + required: false, + name: :api_url, + type: String, + desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com' }, { required: true, diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 332f233bf5e56671515b430a638252ca6033d3e2..2e1b243c2db7cb7c9f92495a77bb522094674632 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -137,7 +137,10 @@ module API expose :web_url expose :request_access_enabled expose :full_name, :full_path - expose :parent_id + + if ::Group.supports_nested_groups? + expose :parent_id + end expose :statistics, if: :statistics do with_options format_with: -> (value) { value.to_i } do diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb index 6187445fc8d9a3028d8513292b24dd447155bf86..2c52d21fa1c1af724bfa933d75b97bbafd574499 100644 --- a/lib/api/v3/groups.rb +++ b/lib/api/v3/groups.rb @@ -74,7 +74,11 @@ module API params do requires :name, type: String, desc: 'The name of the group' requires :path, type: String, desc: 'The path of the group' - optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group' + + if ::Group.supports_nested_groups? + optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group' + end + use :optional_params end post do diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb index e4d14bc81686c6cf6d064bab766ac80a4e2407ea..0eaa0de2eefd025d17bac79be4b7a4d5faf9d710 100644 --- a/lib/api/v3/repositories.rb +++ b/lib/api/v3/repositories.rb @@ -72,7 +72,7 @@ module API optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded' optional :format, type: String, desc: 'The archive format' end - get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do + get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do begin send_git_archive user_project.repository, ref: params[:sha], format: params[:format] rescue diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb index 0ea2f97352d6dfde7bd853e1cbb0339075e24033..6fc1d56d7a05c96cebbd72e7d8d624029005aa56 100644 --- a/lib/constraints/group_url_constrainer.rb +++ b/lib/constraints/group_url_constrainer.rb @@ -1,9 +1,9 @@ class GroupUrlConstrainer def matches?(request) - id = request.params[:group_id] || request.params[:id] + full_path = request.params[:group_id] || request.params[:id] - return false unless DynamicPathValidator.valid_namespace_path?(id) + return false unless DynamicPathValidator.valid_group_path?(full_path) - Group.find_by_full_path(id, follow_redirects: request.get?).present? + Group.find_by_full_path(full_path, follow_redirects: request.get?).present? end end diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb index 4444a1abee320f8af5df49e8b77f719589d5fe54..4c0aee6c48f48cd90f8181c46048ef97a9503646 100644 --- a/lib/constraints/project_url_constrainer.rb +++ b/lib/constraints/project_url_constrainer.rb @@ -2,7 +2,7 @@ class ProjectUrlConstrainer def matches?(request) namespace_path = request.params[:namespace_id] project_path = request.params[:project_id] || request.params[:id] - full_path = namespace_path + '/' + project_path + full_path = [namespace_path, project_path].join('/') return false unless DynamicPathValidator.valid_project_path?(full_path) diff --git a/lib/constraints/user_url_constrainer.rb b/lib/constraints/user_url_constrainer.rb index 28159dc0decee18a4a6ced997bbb462db83964cb..d16ae7f3f40ca7b6f1ac1f1a58f44378f786c037 100644 --- a/lib/constraints/user_url_constrainer.rb +++ b/lib/constraints/user_url_constrainer.rb @@ -1,5 +1,9 @@ class UserUrlConstrainer def matches?(request) - User.find_by_full_path(request.params[:username], follow_redirects: request.get?).present? + full_path = request.params[:username] + + return false unless DynamicPathValidator.valid_user_path?(full_path) + + User.find_by_full_path(full_path, follow_redirects: request.get?).present? end end diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb index fa462cbe095054545a1ccd7fde7de5cab1087727..c4c0623df6c559df9e761da009dee6dd77662a5d 100644 --- a/lib/gitlab/ci/trace/stream.rb +++ b/lib/gitlab/ci/trace/stream.rb @@ -73,7 +73,7 @@ module Gitlab match = "" - stream.each_line do |line| + reverse_line do |line| matches = line.scan(regex) next unless matches.is_a?(Array) next if matches.empty? @@ -86,34 +86,39 @@ module Gitlab nil rescue # if bad regex or something goes wrong we dont want to interrupt transition - # so we just silentrly ignore error for now + # so we just silently ignore error for now end private - def read_last_lines(last_lines) - chunks = [] - pos = lines = 0 - max = stream.size - - # We want an extra line to make sure fist line has full contents - while lines <= last_lines && pos < max - pos += BUFFER_SIZE - - buf = - if pos <= max - stream.seek(-pos, IO::SEEK_END) - stream.read(BUFFER_SIZE) - else # Reached the head, read only left - stream.seek(0) - stream.read(BUFFER_SIZE - (pos - max)) - end - - lines += buf.count("\n") - chunks.unshift(buf) + def read_last_lines(limit) + to_enum(:reverse_line).first(limit).reverse.join + end + + def reverse_line + stream.seek(0, IO::SEEK_END) + debris = '' + + until (buf = read_backward(BUFFER_SIZE)).empty? + buf += debris + debris, *lines = buf.each_line.to_a + lines.reverse_each do |line| + yield(line.force_encoding('UTF-8')) + end end - chunks.join.lines.last(last_lines).join + yield(debris.force_encoding('UTF-8')) unless debris.empty? + end + + def read_backward(length) + cur_offset = stream.tell + start = cur_offset - length + start = 0 if start < 0 + + stream.seek(start, IO::SEEK_SET) + stream.read(cur_offset - start).tap do + stream.seek(start, IO::SEEK_SET) + end end end end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index e76c9abbe0402998ad106f8e957b7b77cc85f85e..a412bb6dbd2cc9d17d929d9e19ab00a65c671263 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -42,7 +42,7 @@ module Gitlab 'in the body of your migration class' end - if Database.postgresql? + if supports_drop_index_concurrently? options = options.merge({ algorithm: :concurrently }) disable_statement_timeout end @@ -50,6 +50,39 @@ module Gitlab remove_index(table_name, options.merge({ column: column_name })) end + # Removes an existing index, concurrently when supported + # + # On PostgreSQL this method removes an index concurrently. + # + # Example: + # + # remove_concurrent_index :users, "index_X_by_Y" + # + # See Rails' `remove_index` for more info on the available arguments. + def remove_concurrent_index_by_name(table_name, index_name, options = {}) + if transaction_open? + raise 'remove_concurrent_index_by_name can not be run inside a transaction, ' \ + 'you can disable transactions by calling disable_ddl_transaction! ' \ + 'in the body of your migration class' + end + + if supports_drop_index_concurrently? + options = options.merge({ algorithm: :concurrently }) + disable_statement_timeout + end + + remove_index(table_name, options.merge({ name: index_name })) + end + + # Only available on Postgresql >= 9.2 + def supports_drop_index_concurrently? + return false unless Database.postgresql? + + version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i + + version >= 90200 + end + # Adds a foreign key with only minimal locking on the tables involved. # # This method only requires minimal locking when using PostgreSQL. When diff --git a/lib/gitlab/dependency_linker.rb b/lib/gitlab/dependency_linker.rb index c45ae8feb2c046cd1a8f959ee43fcbf8315b5b01..3192bf6f667632bba85a34f879a8e8fa8b0662f8 100644 --- a/lib/gitlab/dependency_linker.rb +++ b/lib/gitlab/dependency_linker.rb @@ -1,7 +1,16 @@ module Gitlab module DependencyLinker LINKERS = [ - GemfileLinker + GemfileLinker, + GemspecLinker, + PackageJsonLinker, + ComposerJsonLinker, + PodfileLinker, + PodspecLinker, + PodspecJsonLinker, + CartfileLinker, + GodepsJsonLinker, + RequirementsTxtLinker ].freeze def self.linker(blob_name) diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb index 5f4027e7e814923a7e6efed5355960a5665f4f7d..7bbd154eb03f8f7a49e4074c40b9372d8b20e875 100644 --- a/lib/gitlab/dependency_linker/base_linker.rb +++ b/lib/gitlab/dependency_linker/base_linker.rb @@ -1,6 +1,9 @@ module Gitlab module DependencyLinker class BaseLinker + URL_REGEX = %r{https?://[^'" ]+}.freeze + REPO_REGEX = %r{[^/'" ]+/[^/'" ]+}.freeze + class_attribute :file_type def self.support?(blob_name) @@ -26,59 +29,20 @@ module Gitlab private - def package_url(name) - raise NotImplementedError - end - def link_dependencies raise NotImplementedError end - def package_link(name, url = package_url(name)) - return name unless url - - %{<a href="#{ERB::Util.html_escape_once(url)}" rel="noopener noreferrer" target="_blank">#{ERB::Util.html_escape_once(name)}</a>} + def license_url(name) + Licensee::License.find(name)&.url end - # Links package names in a method call or assignment string argument. - # - # Example: - # link_method_call("gem") - # # Will link `package` in `gem "package"`, `gem("package")` and `gem = "package"` - # - # link_method_call("gem", "specific_package") - # # Will link `specific_package` in `gem "specific_package"` - # - # link_method_call("github", /[^\/]+\/[^\/]+/) - # # Will link `user/repo` in `github "user/repo"`, but not `github "package"` - # - # link_method_call(%w[add_dependency add_development_dependency]) - # # Will link `spec.add_dependency "package"` and `spec.add_development_dependency "package"` - # - # link_method_call("name") - # # Will link `package` in `self.name = "package"` - def link_method_call(method_names, value = nil, &url_proc) - value = - case value - when String - Regexp.escape(value) - when nil - /[^'"]+/ - else - value - end - - method_names = Array(method_names).map { |name| Regexp.escape(name) } - - regex = %r{ - #{Regexp.union(method_names)} # Method name - \s* # Whitespace - [(=]? # Opening brace or equals sign - \s* # Whitespace - ['"](?<name>#{value})['"] # Package name in quotes - }x + def github_url(name) + "https://github.com/#{name}" + end - link_regex(regex, &url_proc) + def link_tag(name, url) + %{<a href="#{ERB::Util.html_escape_once(url)}" rel="nofollow noreferrer noopener" target="_blank">#{ERB::Util.html_escape_once(name)}</a>} end # Links package names based on regex. @@ -86,13 +50,13 @@ module Gitlab # Example: # link_regex(/(github:|:github =>)\s*['"](?<name>[^'"]+)['"]/) # # Will link `user/repo` in `github: "user/repo"` or `:github => "user/repo"` - def link_regex(regex) + def link_regex(regex, &url_proc) highlighted_lines.map!.with_index do |rich_line, i| marker = StringRegexMarker.new(plain_lines[i], rich_line.html_safe) marker.mark(regex, group: :name) do |text, left:, right:| - url = block_given? ? yield(text) : package_url(text) - package_link(text, url) + url = yield(text) + url ? link_tag(text, url) : text end end end @@ -104,6 +68,19 @@ module Gitlab def highlighted_lines @highlighted_lines ||= highlighted_text.lines end + + def regexp_for_value(value, default: /[^'" ]+/) + case value + when Array + Regexp.union(value.map { |v| regexp_for_value(v, default: default) }) + when String + Regexp.escape(value) + when Regexp + value + else + default + end + end end end end diff --git a/lib/gitlab/dependency_linker/cartfile_linker.rb b/lib/gitlab/dependency_linker/cartfile_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..4f69f2c4ab269de654cc34955f2dce6c1dcdcde2 --- /dev/null +++ b/lib/gitlab/dependency_linker/cartfile_linker.rb @@ -0,0 +1,14 @@ +module Gitlab + module DependencyLinker + class CartfileLinker < MethodLinker + self.file_type = :cartfile + + private + + def link_dependencies + link_method_call('github', REPO_REGEX, &method(:github_url)) + link_method_call(%w[github git binary], URL_REGEX, &:itself) + end + end + end +end diff --git a/lib/gitlab/dependency_linker/cocoapods.rb b/lib/gitlab/dependency_linker/cocoapods.rb new file mode 100644 index 0000000000000000000000000000000000000000..2fbde7da1b4f0b017f91a1ec3882b2b066c49043 --- /dev/null +++ b/lib/gitlab/dependency_linker/cocoapods.rb @@ -0,0 +1,10 @@ +module Gitlab + module DependencyLinker + module Cocoapods + def package_url(name) + package = name.split("/", 2).first + "https://cocoapods.org/pods/#{package}" + end + end + end +end diff --git a/lib/gitlab/dependency_linker/composer_json_linker.rb b/lib/gitlab/dependency_linker/composer_json_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..0245bf4077a4abe0fa8f119cbfa97f5952520078 --- /dev/null +++ b/lib/gitlab/dependency_linker/composer_json_linker.rb @@ -0,0 +1,18 @@ +module Gitlab + module DependencyLinker + class ComposerJsonLinker < PackageJsonLinker + self.file_type = :composer_json + + private + + def link_packages + link_packages_at_key("require", &method(:package_url)) + link_packages_at_key("require-dev", &method(:package_url)) + end + + def package_url(name) + "https://packagist.org/packages/#{name}" if name =~ %r{\A#{REPO_REGEX}\z} + end + end + end +end diff --git a/lib/gitlab/dependency_linker/gemfile_linker.rb b/lib/gitlab/dependency_linker/gemfile_linker.rb index 9b82e12652872ffa6031eb7c7701229de8d364f4..d034ea67387d8572dc5bd140cfa2d9a890d4988d 100644 --- a/lib/gitlab/dependency_linker/gemfile_linker.rb +++ b/lib/gitlab/dependency_linker/gemfile_linker.rb @@ -1,28 +1,31 @@ module Gitlab module DependencyLinker - class GemfileLinker < BaseLinker + class GemfileLinker < MethodLinker self.file_type = :gemfile private def link_dependencies - # Link `gem "package_name"` to https://rubygems.org/gems/package_name - link_method_call("gem") + link_urls + link_packages + end + def link_urls # Link `github: "user/repo"` to https://github.com/user/repo - link_regex(/(github:|:github\s*=>)\s*['"](?<name>[^'"]+)['"]/) do |name| - "https://github.com/#{name}" - end + link_regex(/(github:|:github\s*=>)\s*['"](?<name>[^'"]+)['"]/, &method(:github_url)) # Link `git: "https://gitlab.example.com/user/repo"` to https://gitlab.example.com/user/repo - link_regex(%r{(git:|:git\s*=>)\s*['"](?<name>https?://[^'"]+)['"]}) { |url| url } + link_regex(%r{(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]}, &:itself) # Link `source "https://rubygems.org"` to https://rubygems.org - link_method_call("source", %r{https?://[^'"]+}) { |url| url } + link_method_call('source', URL_REGEX, &:itself) end - def package_url(name) - "https://rubygems.org/gems/#{name}" + def link_packages + # Link `gem "package_name"` to https://rubygems.org/gems/package_name + link_method_call('gem') do |name| + "https://rubygems.org/gems/#{name}" + end end end end diff --git a/lib/gitlab/dependency_linker/gemspec_linker.rb b/lib/gitlab/dependency_linker/gemspec_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..f1783ee2ab4ec6963026773f27fbfbadb867b998 --- /dev/null +++ b/lib/gitlab/dependency_linker/gemspec_linker.rb @@ -0,0 +1,18 @@ +module Gitlab + module DependencyLinker + class GemspecLinker < MethodLinker + self.file_type = :gemspec + + private + + def link_dependencies + link_method_call('homepage', URL_REGEX, &:itself) + link_method_call('license', &method(:license_url)) + + link_method_call(%w[name add_dependency add_runtime_dependency add_development_dependency]) do |name| + "https://rubygems.org/gems/#{name}" + end + end + end + end +end diff --git a/lib/gitlab/dependency_linker/godeps_json_linker.rb b/lib/gitlab/dependency_linker/godeps_json_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..fe091baee6dd369cf99161783e93d7598c4a62dc --- /dev/null +++ b/lib/gitlab/dependency_linker/godeps_json_linker.rb @@ -0,0 +1,26 @@ +module Gitlab + module DependencyLinker + class GodepsJsonLinker < JsonLinker + NESTED_REPO_REGEX = %r{([^/]+/)+[^/]+?}.freeze + + self.file_type = :godeps_json + + private + + def link_dependencies + link_json('ImportPath') do |path| + case path + when %r{\A(?<repo>gitlab\.com/#{NESTED_REPO_REGEX})\.git/(?<path>.+)\z}, + %r{\A(?<repo>git(lab|hub)\.com/#{REPO_REGEX})/(?<path>.+)\z} + + "https://#{$~[:repo]}/tree/master/#{$~[:path]}" + when /\Agolang\.org/ + "https://godoc.org/#{path}" + else + "https://#{path}" + end + end + end + end + end +end diff --git a/lib/gitlab/dependency_linker/json_linker.rb b/lib/gitlab/dependency_linker/json_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..a8ef25233d8a816c781b33fa97ff253242880105 --- /dev/null +++ b/lib/gitlab/dependency_linker/json_linker.rb @@ -0,0 +1,44 @@ +module Gitlab + module DependencyLinker + class JsonLinker < BaseLinker + def link + return highlighted_text unless json + + super + end + + private + + # Links package names in a JSON key or values. + # + # Example: + # link_json('name') + # # Will link `package` in `"name": "package"` + # + # link_json('name', 'specific_package') + # # Will link `specific_package` in `"name": "specific_package"` + # + # link_json('name', /[^\/]+\/[^\/]+/) + # # Will link `user/repo` in `"name": "user/repo"`, but not `"name": "package"` + # + # link_json('specific_package', '1.0.1', link: :key) + # # Will link `specific_package` in `"specific_package": "1.0.1"` + def link_json(key, value = nil, link: :value, &url_proc) + key = regexp_for_value(key, default: /[^" ]+/) + value = regexp_for_value(value, default: /[^" ]+/) + + if link == :value + value = /(?<name>#{value})/ + else + key = /(?<name>#{key})/ + end + + link_regex(/"#{key}":\s*"#{value}"/, &url_proc) + end + + def json + @json ||= JSON.parse(plain_text) rescue nil + end + end + end +end diff --git a/lib/gitlab/dependency_linker/method_linker.rb b/lib/gitlab/dependency_linker/method_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..0ffa2a83c93f544eac274647a435127a60811e46 --- /dev/null +++ b/lib/gitlab/dependency_linker/method_linker.rb @@ -0,0 +1,39 @@ +module Gitlab + module DependencyLinker + class MethodLinker < BaseLinker + private + + # Links package names in a method call or assignment string argument. + # + # Example: + # link_method_call('gem') + # # Will link `package` in `gem "package"`, `gem("package")` and `gem = "package"` + # + # link_method_call('gem', 'specific_package') + # # Will link `specific_package` in `gem "specific_package"` + # + # link_method_call('github', /[^\/"]+\/[^\/"]+/) + # # Will link `user/repo` in `github "user/repo"`, but not `github "package"` + # + # link_method_call(%w[add_dependency add_development_dependency]) + # # Will link `spec.add_dependency "package"` and `spec.add_development_dependency "package"` + # + # link_method_call('name') + # # Will link `package` in `self.name = "package"` + def link_method_call(method_name, value = nil, &url_proc) + method_name = regexp_for_value(method_name) + value = regexp_for_value(value) + + regex = %r{ + #{method_name} # Method name + \s* # Whitespace + [(=]? # Opening brace or equals sign + \s* # Whitespace + ['"](?<name>#{value})['"] # Package name in quotes + }x + + link_regex(regex, &url_proc) + end + end + end +end diff --git a/lib/gitlab/dependency_linker/package_json_linker.rb b/lib/gitlab/dependency_linker/package_json_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..330c95f088099b2a89a5aaddcb1707a88645d940 --- /dev/null +++ b/lib/gitlab/dependency_linker/package_json_linker.rb @@ -0,0 +1,44 @@ +module Gitlab + module DependencyLinker + class PackageJsonLinker < JsonLinker + self.file_type = :package_json + + private + + def link_dependencies + link_json('name', json["name"], &method(:package_url)) + link_json('license', &method(:license_url)) + link_json(%w[homepage url], URL_REGEX, &:itself) + + link_packages + end + + def link_packages + link_packages_at_key("dependencies", &method(:package_url)) + link_packages_at_key("devDependencies", &method(:package_url)) + end + + def link_packages_at_key(key, &url_proc) + dependencies = json[key] + return unless dependencies + + dependencies.each do |name, version| + link_json(name, version, link: :key, &url_proc) + + link_json(name) do |value| + case value + when /\A#{URL_REGEX}\z/ + value + when /\A#{REPO_REGEX}\z/ + github_url(value) + end + end + end + end + + def package_url(name) + "https://npmjs.com/package/#{name}" + end + end + end +end diff --git a/lib/gitlab/dependency_linker/podfile_linker.rb b/lib/gitlab/dependency_linker/podfile_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..60ad166ea170757a2eb3a8efa7fbefc6f0b7b36c --- /dev/null +++ b/lib/gitlab/dependency_linker/podfile_linker.rb @@ -0,0 +1,15 @@ +module Gitlab + module DependencyLinker + class PodfileLinker < GemfileLinker + include Cocoapods + + self.file_type = :podfile + + private + + def link_packages + link_method_call('pod', &method(:package_url)) + end + end + end +end diff --git a/lib/gitlab/dependency_linker/podspec_json_linker.rb b/lib/gitlab/dependency_linker/podspec_json_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..d82237ed3f166de0bc97f2c379a31523f000fece --- /dev/null +++ b/lib/gitlab/dependency_linker/podspec_json_linker.rb @@ -0,0 +1,32 @@ +module Gitlab + module DependencyLinker + class PodspecJsonLinker < JsonLinker + include Cocoapods + + self.file_type = :podspec_json + + private + + def link_dependencies + link_json('name', json["name"], &method(:package_url)) + link_json('license', &method(:license_url)) + link_json(%w[homepage git], URL_REGEX, &:itself) + + link_packages_at_key("dependencies", &method(:package_url)) + + json["subspecs"]&.each do |subspec| + link_packages_at_key("dependencies", subspec, &method(:package_url)) + end + end + + def link_packages_at_key(key, root = json, &url_proc) + dependencies = root[key] + return unless dependencies + + dependencies.each do |name, _| + link_regex(/"(?<name>#{Regexp.escape(name)})":\s*\[/, &url_proc) + end + end + end + end +end diff --git a/lib/gitlab/dependency_linker/podspec_linker.rb b/lib/gitlab/dependency_linker/podspec_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..a52c7a024399810696ae6aab30c8aae6c97688dc --- /dev/null +++ b/lib/gitlab/dependency_linker/podspec_linker.rb @@ -0,0 +1,24 @@ +module Gitlab + module DependencyLinker + class PodspecLinker < MethodLinker + include Cocoapods + + STRING_REGEX = /['"](?<name>[^'"]+)['"]/.freeze + + self.file_type = :podspec + + private + + def link_dependencies + link_method_call('homepage', URL_REGEX, &:itself) + + link_regex(%r{(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]}, &:itself) + + link_method_call('license', &method(:license_url)) + link_regex(/license\s*=\s*\{\s*(type:|:type\s*=>)\s*#{STRING_REGEX}/, &method(:license_url)) + + link_method_call(%w[name dependency], &method(:package_url)) + end + end + end +end diff --git a/lib/gitlab/dependency_linker/requirements_txt_linker.rb b/lib/gitlab/dependency_linker/requirements_txt_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..2e197e5cd94dd3c90f16080ac9869c532c073677 --- /dev/null +++ b/lib/gitlab/dependency_linker/requirements_txt_linker.rb @@ -0,0 +1,17 @@ +module Gitlab + module DependencyLinker + class RequirementsTxtLinker < BaseLinker + self.file_type = :requirements_txt + + private + + def link_dependencies + link_regex(/^(?<name>(?![a-z+]+:)[^#.-][^ ><=;\[]+)/) do |name| + "https://pypi.python.org/pypi/#{name}" + end + + link_regex(%r{^(?<name>https?://[^ ]+)}, &:itself) + end + end + end +end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index c6bf25b5874ca6fca9780891481d8d2a8288ed02..2aef7fdaa350e33492a8ee05703cdbd83d8ffc1e 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -1,16 +1,17 @@ module Gitlab module Diff class File - attr_reader :diff, :repository, :diff_refs + attr_reader :diff, :repository, :diff_refs, :fallback_diff_refs - delegate :new_file, :deleted_file, :renamed_file, - :old_path, :new_path, :a_mode, :b_mode, + delegate :new_file?, :deleted_file?, :renamed_file?, + :old_path, :new_path, :a_mode, :b_mode, :mode_changed?, :submodule?, :too_large?, :collapsed?, to: :diff, prefix: false - def initialize(diff, repository:, diff_refs: nil) + def initialize(diff, repository:, diff_refs: nil, fallback_diff_refs: nil) @diff = diff @repository = repository @diff_refs = diff_refs + @fallback_diff_refs = fallback_diff_refs end def position(line) @@ -49,24 +50,60 @@ module Gitlab line_code(line) if line end + def old_sha + diff_refs&.base_sha + end + + def new_sha + diff_refs&.head_sha + end + + def content_sha + return old_content_sha if deleted_file? + return @content_sha if defined?(@content_sha) + + refs = diff_refs || fallback_diff_refs + @content_sha = refs&.head_sha + end + def content_commit - return unless diff_refs + return @content_commit if defined?(@content_commit) + + sha = content_sha + @content_commit = repository.commit(sha) if sha + end + + def old_content_sha + return if new_file? + return @old_content_sha if defined?(@old_content_sha) - repository.commit(deleted_file ? old_ref : new_ref) + refs = diff_refs || fallback_diff_refs + @old_content_sha = refs&.base_sha end def old_content_commit - return unless diff_refs + return @old_content_commit if defined?(@old_content_commit) - repository.commit(old_ref) + sha = old_content_sha + @old_content_commit = repository.commit(sha) if sha end - def old_ref - diff_refs.try(:base_sha) + def blob + return @blob if defined?(@blob) + + sha = content_sha + return @blob = nil unless sha + + repository.blob_at(sha, file_path) end - def new_ref - diff_refs.try(:head_sha) + def old_blob + return @old_blob if defined?(@old_blob) + + sha = old_content_sha + return @old_blob = nil unless sha + + @old_blob = repository.blob_at(sha, old_path) end attr_writer :highlighted_diff_lines @@ -85,10 +122,6 @@ module Gitlab @parallel_diff_lines ||= Gitlab::Diff::ParallelDiff.new(self).parallelize end - def mode_changed? - a_mode && b_mode && a_mode != b_mode - end - def raw_diff diff.diff.to_s end @@ -117,20 +150,8 @@ module Gitlab diff_lines.count(&:removed?) end - def old_blob(commit = old_content_commit) - return unless commit - - repository.blob_at(commit.id, old_path) - end - - def blob(commit = content_commit) - return unless commit - - repository.blob_at(commit.id, file_path) - end - def file_identifier - "#{file_path}-#{new_file}-#{deleted_file}-#{renamed_file}" + "#{file_path}-#{new_file?}-#{deleted_file?}-#{renamed_file?}" end end end diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb index 7c32adc6ce771d9aa8f3c352bd27dddf9c40507f..79836a2fbab8a904ff3de1f1f2ad4a38cbec7901 100644 --- a/lib/gitlab/diff/file_collection/base.rb +++ b/lib/gitlab/diff/file_collection/base.rb @@ -2,7 +2,7 @@ module Gitlab module Diff module FileCollection class Base - attr_reader :project, :diff_options, :diff_view, :diff_refs + attr_reader :project, :diff_options, :diff_refs, :fallback_diff_refs delegate :count, :size, :real_size, to: :diff_files @@ -10,14 +10,15 @@ module Gitlab ::Commit.max_diff_options.merge(ignore_whitespace_change: false, no_collapse: false) end - def initialize(diffable, project:, diff_options: nil, diff_refs: nil) + def initialize(diffable, project:, diff_options: nil, diff_refs: nil, fallback_diff_refs: nil) diff_options = self.class.default_options.merge(diff_options || {}) - @diffable = diffable - @diffs = diffable.raw_diffs(diff_options) - @project = project + @diffable = diffable + @diffs = diffable.raw_diffs(diff_options) + @project = project @diff_options = diff_options - @diff_refs = diff_refs + @diff_refs = diff_refs + @fallback_diff_refs = fallback_diff_refs end def diff_files @@ -35,7 +36,7 @@ module Gitlab private def decorate_diff!(diff) - Gitlab::Diff::File.new(diff, repository: project.repository, diff_refs: diff_refs) + Gitlab::Diff::File.new(diff, repository: project.repository, diff_refs: diff_refs, fallback_diff_refs: fallback_diff_refs) end end end diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb index 0bd226ef0504c265bc084506be024a7327a52f2a..9a58b500a2ccc54a23ea85447a2d304699db02a2 100644 --- a/lib/gitlab/diff/file_collection/merge_request_diff.rb +++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb @@ -8,7 +8,8 @@ module Gitlab super(merge_request_diff, project: merge_request_diff.project, diff_options: diff_options, - diff_refs: merge_request_diff.diff_refs) + diff_refs: merge_request_diff.diff_refs, + fallback_diff_refs: merge_request_diff.fallback_diff_refs) end def diff_files diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 7db896522a917ac33867b08230c60a49f167cde4..ed2f541977a5b442bd09cfbb11bb2e6d778908fc 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -3,7 +3,7 @@ module Gitlab class Highlight attr_reader :diff_file, :diff_lines, :raw_lines, :repository - delegate :old_path, :new_path, :old_ref, :new_ref, to: :diff_file, prefix: :diff + delegate :old_path, :new_path, :old_sha, :new_sha, to: :diff_file, prefix: :diff def initialize(diff_lines, repository: nil) @repository = repository @@ -61,12 +61,12 @@ module Gitlab def old_lines return unless diff_file - @old_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_old_ref, diff_old_path) + @old_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_old_sha, diff_old_path) end def new_lines return unless diff_file - @new_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_new_ref, diff_new_path) + @new_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_new_sha, diff_new_path) end end end diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb index 2b0e19b338b4845c93e09c86e644bfdbc38a8b06..cc285162b44c67fb784fc5a5fedf5eb9a0b14bfd 100644 --- a/lib/gitlab/etag_caching/router.rb +++ b/lib/gitlab/etag_caching/router.rb @@ -10,7 +10,7 @@ module Gitlab # - Ending in `issues/id`/realtime_changes` for the `issue_title` route USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes commit pipelines merge_requests new].freeze - RESERVED_WORDS = Gitlab::Regex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES + RESERVED_WORDS = Gitlab::PathRegex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS) ROUTES = [ Gitlab::EtagCaching::Router::Route.new( diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb index 31d1b66b4f726e1d52b0ae5b42096fc407e20976..deade33735489a01e700070f59cc0c0ee0557017 100644 --- a/lib/gitlab/git/diff.rb +++ b/lib/gitlab/git/diff.rb @@ -11,6 +11,10 @@ module Gitlab # Stats properties attr_accessor :new_file, :renamed_file, :deleted_file + alias_method :new_file?, :new_file + alias_method :deleted_file?, :deleted_file + alias_method :renamed_file?, :renamed_file + attr_accessor :too_large # The maximum size of a diff to display. @@ -208,6 +212,10 @@ module Gitlab hash end + def mode_changed? + a_mode && b_mode && a_mode != b_mode + end + def submodule? a_mode == '160000' || b_mode == '160000' end diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb index bcbad8ec829d76270e90d1d06b1c19bf0e93bb4f..898a5ae15f2ec2d8661c867045fe440d7e6b7d91 100644 --- a/lib/gitlab/git/diff_collection.rb +++ b/lib/gitlab/git/diff_collection.rb @@ -19,22 +19,19 @@ module Gitlab @line_count = 0 @byte_count = 0 @overflow = false + @empty = true @array = Array.new end def each(&block) - if @populated - # @iterator.each is slower than just iterating the array in place - @array.each(&block) - else - Gitlab::GitalyClient.migrate(:commit_raw_diffs) do - each_patch(&block) - end + Gitlab::GitalyClient.migrate(:commit_raw_diffs) do + each_patch(&block) end end def empty? - !@iterator.any? + any? # Make sure the iterator has been exercised + @empty end def overflow? @@ -60,17 +57,17 @@ module Gitlab collection = each_with_index do |element, i| @array[i] = yield(element) end - @populated = true collection end + alias_method :to_ary, :to_a + private def populate! return if @populated each { nil } # force a loop through all diffs - @populated = true nil end @@ -79,15 +76,17 @@ module Gitlab end def each_patch - @iterator.each_with_index do |raw, i| - # First yield cached Diff instances from @array - if @array[i] - yield @array[i] - next - end + i = 0 + @array.each do |diff| + yield diff + i += 1 + end + + return if @overflow + return if @iterator.nil? - # We have exhausted @array, time to create new Diff instances or stop. - break if @overflow + @iterator.each do |raw| + @empty = false if !@all_diffs && i >= @max_files @overflow = true @@ -113,7 +112,13 @@ module Gitlab end yield @array[i] = diff + i += 1 end + + @populated = true + + # Allow iterator to be garbage-collected. It cannot be reused anyway. + @iterator = nil end end end diff --git a/lib/gitlab/group_hierarchy.rb b/lib/gitlab/group_hierarchy.rb new file mode 100644 index 0000000000000000000000000000000000000000..e9d5d52cabb3cea76669c642648480574fe76832 --- /dev/null +++ b/lib/gitlab/group_hierarchy.rb @@ -0,0 +1,104 @@ +module Gitlab + # Retrieving of parent or child groups based on a base ActiveRecord relation. + # + # This class uses recursive CTEs and as a result will only work on PostgreSQL. + class GroupHierarchy + attr_reader :base, :model + + # base - An instance of ActiveRecord::Relation for which to get parent or + # child groups. + def initialize(base) + @base = base + @model = base.model + end + + # Returns a relation that includes the base set of groups and all their + # ancestors (recursively). + def base_and_ancestors + return model.none unless Group.supports_nested_groups? + + base_and_ancestors_cte.apply_to(model.all) + end + + # Returns a relation that includes the base set of groups and all their + # descendants (recursively). + def base_and_descendants + return model.none unless Group.supports_nested_groups? + + base_and_descendants_cte.apply_to(model.all) + end + + # Returns a relation that includes the base groups, their ancestors, and the + # descendants of the base groups. + # + # The resulting query will roughly look like the following: + # + # WITH RECURSIVE ancestors AS ( ... ), + # descendants AS ( ... ) + # SELECT * + # FROM ( + # SELECT * + # FROM ancestors namespaces + # + # UNION + # + # SELECT * + # FROM descendants namespaces + # ) groups; + # + # Using this approach allows us to further add criteria to the relation with + # Rails thinking it's selecting data the usual way. + def all_groups + return base unless Group.supports_nested_groups? + + ancestors = base_and_ancestors_cte + descendants = base_and_descendants_cte + + ancestors_table = ancestors.alias_to(groups_table) + descendants_table = descendants.alias_to(groups_table) + + union = SQL::Union.new([model.unscoped.from(ancestors_table), + model.unscoped.from(descendants_table)]) + + model. + unscoped. + with. + recursive(ancestors.to_arel, descendants.to_arel). + from("(#{union.to_sql}) #{model.table_name}") + end + + private + + def base_and_ancestors_cte + cte = SQL::RecursiveCTE.new(:base_and_ancestors) + + cte << base.except(:order) + + # Recursively get all the ancestors of the base set. + cte << model. + from([groups_table, cte.table]). + where(groups_table[:id].eq(cte.table[:parent_id])). + except(:order) + + cte + end + + def base_and_descendants_cte + cte = SQL::RecursiveCTE.new(:base_and_descendants) + + cte << base.except(:order) + + # Recursively get all the descendants of the base set. + cte << model. + from([groups_table, cte.table]). + where(groups_table[:parent_id].eq(cte.table[:id])). + except(:order) + + cte + end + + def groups_table + model.arel_table + end + end +end diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb index df962d203b7abf58f1a2b875cb7a86873735524c..e78b7f22e0374ed8a43518eb2c0ebae1efa7749b 100644 --- a/lib/gitlab/health_checks/fs_shards_check.rb +++ b/lib/gitlab/health_checks/fs_shards_check.rb @@ -2,6 +2,9 @@ module Gitlab module HealthChecks class FsShardsCheck extend BaseAbstractCheck + RANDOM_STRING = SecureRandom.hex(1000).freeze + COMMAND_TIMEOUT = '1'.freeze + TIMEOUT_EXECUTABLE = 'timeout'.freeze class << self def readiness @@ -41,8 +44,6 @@ module Gitlab private - RANDOM_STRING = SecureRandom.hex(1000).freeze - def operation_metrics(ok_metric, latency_metric, operation, **labels) with_timing operation do |result, elapsed| [ @@ -63,8 +64,8 @@ module Gitlab @storage_paths ||= Gitlab.config.repositories.storages end - def with_timeout(args) - %w{timeout 1}.concat(args) + def exec_with_timeout(cmd_args, *args, &block) + Gitlab::Popen.popen([TIMEOUT_EXECUTABLE, COMMAND_TIMEOUT].concat(cmd_args), *args, &block) end def tmp_file_path(storage_name) @@ -78,7 +79,7 @@ module Gitlab def storage_stat_test(storage_name) stat_path = File.join(path(storage_name), '.') begin - _, status = Gitlab::Popen.popen(with_timeout(%W{ stat #{stat_path} })) + _, status = exec_with_timeout(%W{ stat #{stat_path} }) status == 0 rescue Errno::ENOENT File.exist?(stat_path) && File::Stat.new(stat_path).readable? @@ -86,7 +87,7 @@ module Gitlab end def storage_write_test(tmp_path) - _, status = Gitlab::Popen.popen(with_timeout(%W{ tee #{tmp_path} })) do |stdin| + _, status = exec_with_timeout(%W{ tee #{tmp_path} }) do |stdin| stdin.write(RANDOM_STRING) end status == 0 @@ -96,7 +97,7 @@ module Gitlab end def storage_read_test(tmp_path) - _, status = Gitlab::Popen.popen(with_timeout(%W{ diff #{tmp_path} - })) do |stdin| + _, status = exec_with_timeout(%W{ diff #{tmp_path} - }) do |stdin| stdin.write(RANDOM_STRING) end status == 0 @@ -106,7 +107,7 @@ module Gitlab end def delete_test_file(tmp_path) - _, status = Gitlab::Popen.popen(with_timeout(%W{ rm -f #{tmp_path} })) + _, status = exec_with_timeout(%W{ rm -f #{tmp_path} }) status == 0 rescue Errno::ENOENT File.delete(tmp_path) rescue Errno::ENOENT diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb index 3411516319f8825c5464209543e41caeb09d7145..5ab3eeb3aff1e993c3e24aad1318b05c509e6e88 100644 --- a/lib/gitlab/i18n.rb +++ b/lib/gitlab/i18n.rb @@ -12,15 +12,36 @@ module Gitlab AVAILABLE_LANGUAGES.keys end - def set_locale(current_user) - requested_locale = current_user&.preferred_language || ::I18n.default_locale - locale = FastGettext.set_locale(requested_locale) - ::I18n.locale = locale + def locale + FastGettext.locale end - def reset_locale + def locale=(locale_string) + requested_locale = locale_string || ::I18n.default_locale + new_locale = FastGettext.set_locale(requested_locale) + ::I18n.locale = new_locale + end + + def use_default_locale FastGettext.set_locale(::I18n.default_locale) ::I18n.locale = ::I18n.default_locale end + + def with_locale(locale_string) + original_locale = locale + + self.locale = locale_string + yield + ensure + self.locale = original_locale + end + + def with_user_locale(user, &block) + with_locale(user&.preferred_language, &block) + end + + def with_default_locale(&block) + with_locale(::I18n.default_locale, &block) + end end end diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb new file mode 100644 index 0000000000000000000000000000000000000000..1c0abc9f7cfb0b9ea89f796a5a2b2203bd4dc892 --- /dev/null +++ b/lib/gitlab/path_regex.rb @@ -0,0 +1,264 @@ +module Gitlab + module PathRegex + extend self + + # All routes that appear on the top level must be listed here. + # This will make sure that groups cannot be created with these names + # as these routes would be masked by the paths already in place. + # + # Example: + # /api/api-project + # + # the path `api` shouldn't be allowed because it would be masked by `api/*` + # + TOP_LEVEL_ROUTES = %w[ + - + .well-known + abuse_reports + admin + all + api + assets + autocomplete + ci + dashboard + explore + files + groups + health_check + help + hooks + import + invites + issues + jwt + koding + member + merge_requests + new + notes + notification_settings + oauth + profile + projects + public + repository + robots.txt + s + search + sent_notifications + services + snippets + teams + u + unicorn_test + unsubscribes + uploads + users + ].freeze + + # This list should contain all words following `/*namespace_id/:project_id` in + # routes that contain a second wildcard. + # + # Example: + # /*namespace_id/:project_id/badges/*ref/build + # + # If `badges` was allowed as a project/group name, we would not be able to access the + # `badges` route for those projects: + # + # Consider a namespace with path `foo/bar` and a project called `badges`. + # The route to the build badge would then be `/foo/bar/badges/badges/master/build.svg` + # + # When accessing this path the route would be matched to the `badges` path + # with the following params: + # - namespace_id: `foo` + # - project_id: `bar` + # - ref: `badges/master` + # + # Failing to find the project, this would result in a 404. + # + # By rejecting `badges` the router can _count_ on the fact that `badges` will + # be preceded by the `namespace/project`. + PROJECT_WILDCARD_ROUTES = %w[ + badges + blame + blob + builds + commits + create + create_dir + edit + environments/folders + files + find_file + gitlab-lfs/objects + info/lfs/objects + new + preview + raw + refs + tree + update + wikis + ].freeze + + # These are all the paths that follow `/groups/*id/ or `/groups/*group_id` + # We need to reject these because we have a `/groups/*id` page that is the same + # as the `/*id`. + # + # If we would allow a subgroup to be created with the name `activity` then + # this group would not be accessible through `/groups/parent/activity` since + # this would map to the activity-page of its parent. + GROUP_ROUTES = %w[ + activity + analytics + audit_events + avatar + edit + group_members + hooks + issues + labels + ldap + ldap_group_links + merge_requests + milestones + notification_setting + pipeline_quota + projects + subgroups + ].freeze + + ILLEGAL_PROJECT_PATH_WORDS = PROJECT_WILDCARD_ROUTES + ILLEGAL_GROUP_PATH_WORDS = (PROJECT_WILDCARD_ROUTES | GROUP_ROUTES).freeze + + # The namespace regex is used in JavaScript to validate usernames in the "Register" form. However, Javascript + # does not support the negative lookbehind assertion (?<!) that disallows usernames ending in `.git` and `.atom`. + # Since this is a non-trivial problem to solve in Javascript (heavily complicate the regex, modify view code to + # allow non-regex validations, etc), `NAMESPACE_FORMAT_REGEX_JS` serves as a Javascript-compatible version of + # `NAMESPACE_FORMAT_REGEX`, with the negative lookbehind assertion removed. This means that the client-side validation + # will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation. + PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze + NAMESPACE_FORMAT_REGEX_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze + + NO_SUFFIX_REGEX = /(?<!\.git|\.atom)/.freeze + NAMESPACE_FORMAT_REGEX = /(?:#{NAMESPACE_FORMAT_REGEX_JS})#{NO_SUFFIX_REGEX}/.freeze + PROJECT_PATH_FORMAT_REGEX = /(?:#{PATH_REGEX_STR})#{NO_SUFFIX_REGEX}/.freeze + FULL_NAMESPACE_FORMAT_REGEX = %r{(#{NAMESPACE_FORMAT_REGEX}/)*#{NAMESPACE_FORMAT_REGEX}}.freeze + + def root_namespace_route_regex + @root_namespace_route_regex ||= begin + illegal_words = Regexp.new(Regexp.union(TOP_LEVEL_ROUTES).source, Regexp::IGNORECASE) + + single_line_regexp %r{ + (?!(#{illegal_words})/) + #{NAMESPACE_FORMAT_REGEX} + }x + end + end + + def full_namespace_route_regex + @full_namespace_route_regex ||= begin + illegal_words = Regexp.new(Regexp.union(ILLEGAL_GROUP_PATH_WORDS).source, Regexp::IGNORECASE) + + single_line_regexp %r{ + #{root_namespace_route_regex} + (?: + / + (?!#{illegal_words}/) + #{NAMESPACE_FORMAT_REGEX} + )* + }x + end + end + + def project_route_regex + @project_route_regex ||= begin + illegal_words = Regexp.new(Regexp.union(ILLEGAL_PROJECT_PATH_WORDS).source, Regexp::IGNORECASE) + + single_line_regexp %r{ + (?!(#{illegal_words})/) + #{PROJECT_PATH_FORMAT_REGEX} + }x + end + end + + def project_git_route_regex + @project_git_route_regex ||= /#{project_route_regex}\.git/.freeze + end + + def root_namespace_path_regex + @root_namespace_path_regex ||= %r{\A#{root_namespace_route_regex}/\z} + end + + def full_namespace_path_regex + @full_namespace_path_regex ||= %r{\A#{full_namespace_route_regex}/\z} + end + + def project_path_regex + @project_path_regex ||= %r{\A#{project_route_regex}/\z} + end + + def full_project_path_regex + @full_project_path_regex ||= %r{\A#{full_namespace_route_regex}/#{project_route_regex}/\z} + end + + def full_namespace_format_regex + @namespace_format_regex ||= /A#{FULL_NAMESPACE_FORMAT_REGEX}\z/.freeze + end + + def namespace_format_regex + @namespace_format_regex ||= /\A#{NAMESPACE_FORMAT_REGEX}\z/.freeze + end + + def namespace_format_message + "can contain only letters, digits, '_', '-' and '.'. " \ + "Cannot start with '-' or end in '.', '.git' or '.atom'." \ + end + + def project_path_format_regex + @project_path_format_regex ||= /\A#{PROJECT_PATH_FORMAT_REGEX}\z/.freeze + end + + def project_path_format_message + "can contain only letters, digits, '_', '-' and '.'. " \ + "Cannot start with '-', end in '.git' or end in '.atom'" \ + end + + def archive_formats_regex + # |zip|tar| tar.gz | tar.bz2 | + @archive_formats_regex ||= /(zip|tar|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/.freeze + end + + def git_reference_regex + # Valid git ref regex, see: + # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html + + @git_reference_regex ||= single_line_regexp %r{ + (?! + (?# doesn't begins with) + \/| (?# rule #6) + (?# doesn't contain) + .*(?: + [\/.]\.| (?# rule #1,3) + \/\/| (?# rule #6) + @\{| (?# rule #8) + \\ (?# rule #9) + ) + ) + [^\000-\040\177~^:?*\[]+ (?# rule #4-5) + (?# doesn't end with) + (?<!\.lock) (?# rule #1) + (?<![\/.]) (?# rule #6-7) + }x + end + + private + + def single_line_regexp(regex) + # Turns a multiline extended regexp into a single line one, + # beacuse `rake routes` breaks on multiline regexes. + Regexp.new(regex.source.gsub(/\(\?#.+?\)/, '').gsub(/\s*/, ''), regex.options ^ Regexp::EXTENDED).freeze + end + end +end diff --git a/lib/gitlab/project_authorizations/with_nested_groups.rb b/lib/gitlab/project_authorizations/with_nested_groups.rb new file mode 100644 index 0000000000000000000000000000000000000000..bb0df1e3dad25af485134213816026fe1e77739c --- /dev/null +++ b/lib/gitlab/project_authorizations/with_nested_groups.rb @@ -0,0 +1,125 @@ +module Gitlab + module ProjectAuthorizations + # Calculating new project authorizations when supporting nested groups. + # + # This class relies on Common Table Expressions to efficiently get all data, + # including data for nested groups. As a result this class can only be used + # on PostgreSQL. + class WithNestedGroups + attr_reader :user + + # user - The User object for which to calculate the authorizations. + def initialize(user) + @user = user + end + + def calculate + cte = recursive_cte + cte_alias = cte.table.alias(Group.table_name) + projects = Project.arel_table + links = ProjectGroupLink.arel_table + + relations = [ + # The project a user has direct access to. + user.projects.select_for_project_authorization, + + # The personal projects of the user. + user.personal_projects.select_as_master_for_project_authorization, + + # Projects that belong directly to any of the groups the user has + # access to. + Namespace. + unscoped. + select([alias_as_column(projects[:id], 'project_id'), + cte_alias[:access_level]]). + from(cte_alias). + joins(:projects), + + # Projects shared with any of the namespaces the user has access to. + Namespace. + unscoped. + select([links[:project_id], + least(cte_alias[:access_level], + links[:group_access], + 'access_level')]). + from(cte_alias). + joins('INNER JOIN project_group_links ON project_group_links.group_id = namespaces.id'). + joins('INNER JOIN projects ON projects.id = project_group_links.project_id'). + joins('INNER JOIN namespaces p_ns ON p_ns.id = projects.namespace_id'). + where('p_ns.share_with_group_lock IS FALSE') + ] + + union = Gitlab::SQL::Union.new(relations) + + ProjectAuthorization. + unscoped. + with. + recursive(cte.to_arel). + select_from_union(union) + end + + private + + # Builds a recursive CTE that gets all the groups the current user has + # access to, including any nested groups. + def recursive_cte + cte = Gitlab::SQL::RecursiveCTE.new(:namespaces_cte) + members = Member.arel_table + namespaces = Namespace.arel_table + + # Namespaces the user is a member of. + cte << user.groups. + select([namespaces[:id], members[:access_level]]). + except(:order) + + # Sub groups of any groups the user is a member of. + cte << Group.select([namespaces[:id], + greatest(members[:access_level], + cte.table[:access_level], 'access_level')]). + joins(join_cte(cte)). + joins(join_members). + except(:order) + + cte + end + + # Builds a LEFT JOIN to join optional memberships onto the CTE. + def join_members + members = Member.arel_table + namespaces = Namespace.arel_table + + cond = members[:source_id]. + eq(namespaces[:id]). + and(members[:source_type].eq('Namespace')). + and(members[:requested_at].eq(nil)). + and(members[:user_id].eq(user.id)) + + Arel::Nodes::OuterJoin.new(members, Arel::Nodes::On.new(cond)) + end + + # Builds an INNER JOIN to join namespaces onto the CTE. + def join_cte(cte) + namespaces = Namespace.arel_table + cond = cte.table[:id].eq(namespaces[:parent_id]) + + Arel::Nodes::InnerJoin.new(cte.table, Arel::Nodes::On.new(cond)) + end + + def greatest(left, right, column_alias) + sql_function('GREATEST', [left, right], column_alias) + end + + def least(left, right, column_alias) + sql_function('LEAST', [left, right], column_alias) + end + + def sql_function(name, args, column_alias) + alias_as_column(Arel::Nodes::NamedFunction.new(name, args), column_alias) + end + + def alias_as_column(value, alias_to) + Arel::Nodes::As.new(value, Arel::Nodes::SqlLiteral.new(alias_to)) + end + end + end +end diff --git a/lib/gitlab/project_authorizations/without_nested_groups.rb b/lib/gitlab/project_authorizations/without_nested_groups.rb new file mode 100644 index 0000000000000000000000000000000000000000..627e8c5fba26f8129418ced650a803972bf7e606 --- /dev/null +++ b/lib/gitlab/project_authorizations/without_nested_groups.rb @@ -0,0 +1,35 @@ +module Gitlab + module ProjectAuthorizations + # Calculating new project authorizations when not supporting nested groups. + class WithoutNestedGroups + attr_reader :user + + # user - The User object for which to calculate the authorizations. + def initialize(user) + @user = user + end + + def calculate + relations = [ + # Projects the user is a direct member of + user.projects.select_for_project_authorization, + + # Personal projects + user.personal_projects.select_as_master_for_project_authorization, + + # Projects of groups the user is a member of + user.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 + ] + + union = Gitlab::SQL::Union.new(relations) + + ProjectAuthorization. + unscoped. + select_from_union(union) + end + end + end +end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index f609850f8fad3747ae544101f9d510f1e3a3a58b..e4d2a9924707c748c8c373fe8dbb0df6558bc336 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -2,203 +2,6 @@ module Gitlab module Regex extend self - # All routes that appear on the top level must be listed here. - # This will make sure that groups cannot be created with these names - # as these routes would be masked by the paths already in place. - # - # Example: - # /api/api-project - # - # the path `api` shouldn't be allowed because it would be masked by `api/*` - # - TOP_LEVEL_ROUTES = %w[ - - - .well-known - abuse_reports - admin - all - api - assets - autocomplete - ci - dashboard - explore - files - groups - health_check - help - hooks - import - invites - issues - jwt - koding - member - merge_requests - new - notes - notification_settings - oauth - profile - projects - public - repository - robots.txt - s - search - sent_notifications - services - snippets - teams - u - unicorn_test - unsubscribes - uploads - users - ].freeze - - # This list should contain all words following `/*namespace_id/:project_id` in - # routes that contain a second wildcard. - # - # Example: - # /*namespace_id/:project_id/badges/*ref/build - # - # If `badges` was allowed as a project/group name, we would not be able to access the - # `badges` route for those projects: - # - # Consider a namespace with path `foo/bar` and a project called `badges`. - # The route to the build badge would then be `/foo/bar/badges/badges/master/build.svg` - # - # When accessing this path the route would be matched to the `badges` path - # with the following params: - # - namespace_id: `foo` - # - project_id: `bar` - # - ref: `badges/master` - # - # Failing to find the project, this would result in a 404. - # - # By rejecting `badges` the router can _count_ on the fact that `badges` will - # be preceded by the `namespace/project`. - PROJECT_WILDCARD_ROUTES = %w[ - badges - blame - blob - builds - commits - create - create_dir - edit - environments/folders - files - find_file - gitlab-lfs/objects - info/lfs/objects - new - preview - raw - refs - tree - update - wikis - ].freeze - - # These are all the paths that follow `/groups/*id/ or `/groups/*group_id` - # We need to reject these because we have a `/groups/*id` page that is the same - # as the `/*id`. - # - # If we would allow a subgroup to be created with the name `activity` then - # this group would not be accessible through `/groups/parent/activity` since - # this would map to the activity-page of its parent. - GROUP_ROUTES = %w[ - activity - analytics - audit_events - avatar - edit - group_members - hooks - issues - labels - ldap - ldap_group_links - merge_requests - milestones - notification_setting - pipeline_quota - projects - subgroups - ].freeze - - ILLEGAL_PROJECT_PATH_WORDS = PROJECT_WILDCARD_ROUTES - ILLEGAL_GROUP_PATH_WORDS = (PROJECT_WILDCARD_ROUTES | GROUP_ROUTES).freeze - - # The namespace regex is used in Javascript to validate usernames in the "Register" form. However, Javascript - # does not support the negative lookbehind assertion (?<!) that disallows usernames ending in `.git` and `.atom`. - # Since this is a non-trivial problem to solve in Javascript (heavily complicate the regex, modify view code to - # allow non-regex validatiions, etc), `NAMESPACE_REGEX_STR_JS` serves as a Javascript-compatible version of - # `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation - # will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation. - PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze - NAMESPACE_REGEX_STR_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze - NO_SUFFIX_REGEX_STR = '(?<!\.git|\.atom)'.freeze - NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR_JS})#{NO_SUFFIX_REGEX_STR}".freeze - PROJECT_REGEX_STR = "(?:#{PATH_REGEX_STR})#{NO_SUFFIX_REGEX_STR}".freeze - - # Same as NAMESPACE_REGEX_STR but allows `/` in the path. - # So `group/subgroup` will match this regex but not NAMESPACE_REGEX_STR - FULL_NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR}/)*#{NAMESPACE_REGEX_STR}".freeze - - def root_namespace_route_regex - @root_namespace_route_regex ||= begin - illegal_words = Regexp.new(Regexp.union(TOP_LEVEL_ROUTES).source, Regexp::IGNORECASE) - - single_line_regexp %r{ - (?!(#{illegal_words})/) - #{NAMESPACE_REGEX_STR} - }x - end - end - - def root_namespace_path_regex - @root_namespace_path_regex ||= %r{\A#{root_namespace_route_regex}/\z} - end - - def full_namespace_path_regex - @full_namespace_path_regex ||= %r{\A#{namespace_route_regex}/\z} - end - - def full_project_path_regex - @full_project_path_regex ||= %r{\A#{namespace_route_regex}/#{project_route_regex}/\z} - end - - def namespace_regex - @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze - end - - def full_namespace_regex - @full_namespace_regex ||= %r{\A#{FULL_NAMESPACE_REGEX_STR}\z} - end - - def namespace_route_regex - @namespace_route_regex ||= begin - illegal_words = Regexp.new(Regexp.union(ILLEGAL_GROUP_PATH_WORDS).source, Regexp::IGNORECASE) - - single_line_regexp %r{ - #{root_namespace_route_regex} - (?: - / - (?!#{illegal_words}/) - #{NAMESPACE_REGEX_STR} - )* - }x - end - end - - def namespace_regex_message - "can contain only letters, digits, '_', '-' and '.'. " \ - "Cannot start with '-' or end in '.', '.git' or '.atom'." \ - end - def namespace_name_regex @namespace_name_regex ||= /\A[\p{Alnum}\p{Pd}_\. ]*\z/.freeze end @@ -216,34 +19,6 @@ module Gitlab "It must start with letter, digit, emoji or '_'." end - def project_path_regex - @project_path_regex ||= %r{\A#{project_route_regex}/\z} - end - - def project_route_regex - @project_route_regex ||= begin - illegal_words = Regexp.new(Regexp.union(ILLEGAL_PROJECT_PATH_WORDS).source, Regexp::IGNORECASE) - - single_line_regexp %r{ - (?!(#{illegal_words})/) - #{PROJECT_REGEX_STR} - }x - end - end - - def project_git_route_regex - @project_git_route_regex ||= /#{project_route_regex}\.git/.freeze - end - - def project_path_format_regex - @project_path_format_regex ||= /\A#{PROJECT_REGEX_STR}\z/.freeze - end - - def project_path_regex_message - "can contain only letters, digits, '_', '-' and '.'. " \ - "Cannot start with '-', end in '.git' or end in '.atom'" \ - end - def file_name_regex @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@\+]*\z/.freeze end @@ -252,36 +27,8 @@ module Gitlab "can contain only letters, digits, '_', '-', '@', '+' and '.'." end - def archive_formats_regex - # |zip|tar| tar.gz | tar.bz2 | - @archive_formats_regex ||= /(zip|tar|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/.freeze - end - - def git_reference_regex - # Valid git ref regex, see: - # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html - - @git_reference_regex ||= single_line_regexp %r{ - (?! - (?# doesn't begins with) - \/| (?# rule #6) - (?# doesn't contain) - .*(?: - [\/.]\.| (?# rule #1,3) - \/\/| (?# rule #6) - @\{| (?# rule #8) - \\ (?# rule #9) - ) - ) - [^\000-\040\177~^:?*\[]+ (?# rule #4-5) - (?# doesn't end with) - (?<!\.lock) (?# rule #1) - (?<![\/.]) (?# rule #6-7) - }x - end - def container_registry_reference_regex - git_reference_regex + Gitlab::PathRegex.git_reference_regex end ## @@ -315,13 +62,5 @@ module Gitlab "can contain only lowercase letters, digits, and '-'. " \ "Must start with a letter, and cannot end with '-'" end - - private - - def single_line_regexp(regex) - # Turns a multiline extended regexp into a single line one, - # beacuse `rake routes` breaks on multiline regexes. - Regexp.new(regex.source.gsub(/\(\?#.+?\)/, '').gsub(/\s*/, ''), regex.options ^ Regexp::EXTENDED).freeze - end end end diff --git a/lib/gitlab/sql/recursive_cte.rb b/lib/gitlab/sql/recursive_cte.rb new file mode 100644 index 0000000000000000000000000000000000000000..5b1b03820a328ff59cd7a60b4bfe855efcee5178 --- /dev/null +++ b/lib/gitlab/sql/recursive_cte.rb @@ -0,0 +1,62 @@ +module Gitlab + module SQL + # Class for easily building recursive CTE statements. + # + # Example: + # + # cte = RecursiveCTE.new(:my_cte_name) + # ns = Arel::Table.new(:namespaces) + # + # cte << Namespace. + # where(ns[:parent_id].eq(some_namespace_id)) + # + # cte << Namespace. + # from([ns, cte.table]). + # where(ns[:parent_id].eq(cte.table[:id])) + # + # Namespace.with. + # recursive(cte.to_arel). + # from(cte.alias_to(ns)) + class RecursiveCTE + attr_reader :table + + # name - The name of the CTE as a String or Symbol. + def initialize(name) + @table = Arel::Table.new(name) + @queries = [] + end + + # Adds a query to the body of the CTE. + # + # relation - The relation object to add to the body of the CTE. + def <<(relation) + @queries << relation + end + + # Returns the Arel relation for this CTE. + def to_arel + sql = Arel::Nodes::SqlLiteral.new(Union.new(@queries).to_sql) + + Arel::Nodes::As.new(table, Arel::Nodes::Grouping.new(sql)) + end + + # Returns an "AS" statement that aliases the CTE name as the given table + # name. This allows one to trick ActiveRecord into thinking it's selecting + # from an actual table, when in reality it's selecting from a CTE. + # + # alias_table - The Arel table to use as the alias. + def alias_to(alias_table) + Arel::Nodes::As.new(table, alias_table) + end + + # Applies the CTE to the given relation, returning a new one that will + # query from it. + def apply_to(relation) + relation.except(:where). + with. + recursive(to_arel). + from(alias_to(relation.model.arel_table)) + end + end + end +end diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index 9472c3c992f091d9f673282d1d112c6a932df9c7..b341b5a03099a01e1e22d9de8143155ef6d27454 100644 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -87,4 +87,6 @@ shell_path="/bin/bash" # This variable controls whether the init script starts/stops Gitaly gitaly_enabled=false +gitaly_dir=$(cd $app_root/../gitaly 2> /dev/null && pwd) +gitaly_pid_path="$pid_path/gitaly.pid" gitaly_log="$app_root/log/gitaly.log" diff --git a/package.json b/package.json index 800327d8a08c5871fad0fce9c106cab0eeb9932c..8e280e77d67bf015db41070ba1ac2dbfad3396d7 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "jszip-utils": "^0.0.2", "marked": "^0.3.6", "mousetrap": "^1.4.6", + "name-all-modules-plugin": "^1.0.1", "pdfjs-dist": "^1.8.252", "pikaday": "^1.5.1", "prismjs": "^1.6.0", @@ -57,8 +58,8 @@ "vue-loader": "^11.3.4", "vue-resource": "^0.9.3", "vue-template-compiler": "^2.2.6", - "webpack": "^2.3.3", - "webpack-bundle-analyzer": "^2.3.0" + "webpack": "^2.6.1", + "webpack-bundle-analyzer": "^2.8.2" }, "devDependencies": { "babel-plugin-istanbul": "^4.0.0", diff --git a/qa/Dockerfile b/qa/Dockerfile index 72c82503542ad951325a141e713cebf9dfaddee7..9e2a74ef991ec3844011d7188bedc2a5d02daddc 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -1,10 +1,25 @@ FROM ruby:2.3 LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>" -RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list && \ - apt-get update && apt-get install -y --force-yes \ - libqt5webkit5-dev qt5-qmake qt5-default build-essential xvfb git && \ - apt-get clean +## +# Update APT sources and install some dependencies +# +RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list +RUN apt-get update && apt-get install -y wget git unzip xvfb + +## +# At this point Google Chrome Beta is 59 - first version with headless support +# +RUN wget -q https://dl.google.com/linux/direct/google-chrome-beta_current_amd64.deb +RUN dpkg -i google-chrome-beta_current_amd64.deb; apt-get -fy install + +## +# Install chromedriver to make it work with Selenium +# +RUN wget -q https://chromedriver.storage.googleapis.com/2.29/chromedriver_linux64.zip +RUN unzip chromedriver_linux64.zip -d /usr/local/bin + +RUN apt-get clean WORKDIR /home/qa diff --git a/qa/Gemfile b/qa/Gemfile index 6bfe25ba4371b6372bb7743b47f7269fdc923c80..5d089a459346357e978500c46929713c073c5cad 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -2,6 +2,6 @@ source 'https://rubygems.org' gem 'capybara', '~> 2.12.1' gem 'capybara-screenshot', '~> 1.0.14' -gem 'capybara-webkit', '~> 1.12.0' gem 'rake', '~> 12.0.0' gem 'rspec', '~> 3.5' +gem 'selenium-webdriver', '~> 2.53' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 6de2abff1985fa60949cd279bcc08f5d6488f7b6..4dd71aa50106972e5013b2355f2ecfea7ecfe13b 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -16,7 +16,10 @@ GEM capybara-webkit (1.12.0) capybara (>= 2.3.0, < 2.13.0) json + childprocess (0.7.0) + ffi (~> 1.0, >= 1.0.11) diff-lcs (1.3) + ffi (1.9.18) json (2.0.3) launchy (2.4.3) addressable (~> 2.3) @@ -44,6 +47,12 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.5.0) rspec-support (3.5.0) + rubyzip (1.2.1) + selenium-webdriver (2.53.4) + childprocess (~> 0.5) + rubyzip (~> 1.0) + websocket (~> 1.0) + websocket (1.2.4) xpath (2.0.0) nokogiri (~> 1.3) @@ -56,6 +65,7 @@ DEPENDENCIES capybara-webkit (~> 1.12.0) rake (~> 12.0.0) rspec (~> 3.5) + selenium-webdriver (~> 2.53) BUNDLED WITH 1.14.6 diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb index d72187fcd34ff750f417e51d752a0949207a1e88..78a93828d36b17c706befdc0f6ab90740eb6976d 100644 --- a/qa/qa/specs/config.rb +++ b/qa/qa/specs/config.rb @@ -1,7 +1,7 @@ require 'rspec/core' require 'capybara/rspec' -require 'capybara-webkit' require 'capybara-screenshot/rspec' +require 'selenium-webdriver' # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/LineLength @@ -20,7 +20,6 @@ module QA configure_rspec! configure_capybara! - configure_webkit! end def configure_rspec! @@ -43,9 +42,9 @@ module QA config.order = :random Kernel.srand config.seed - config.before(:all) do - page.current_window.resize_to(1200, 1800) - end + # config.before(:all) do + # page.current_window.resize_to(1200, 1800) + # end config.formatter = :documentation config.color = true @@ -53,26 +52,28 @@ module QA end def configure_capybara! + Capybara.register_driver :chrome do |app| + capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( + 'chromeOptions' => { + 'binary' => '/opt/google/chrome-beta/google-chrome-beta', + 'args' => %w[headless no-sandbox disable-gpu] + } + ) + + Capybara::Selenium::Driver + .new(app, browser: :chrome, desired_capabilities: capabilities) + end + Capybara.configure do |config| config.app_host = @address - config.default_driver = :webkit - config.javascript_driver = :webkit + config.default_driver = :chrome + config.javascript_driver = :chrome config.default_max_wait_time = 4 # https://github.com/mattheworiordan/capybara-screenshot/issues/164 config.save_path = 'tmp' end end - - def configure_webkit! - Capybara::Webkit.configure do |config| - config.allow_url(@address) - config.block_unknown_urls - end - rescue RuntimeError # rubocop:disable Lint/HandleExceptions - # TODO, Webkit is already configured, this make this - # configuration step idempotent, should be improved. - end end end end diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index c07a32346734b883388acd66fd0aff64a301d5a2..64d06ef65580fe31e139787931e7d8155b254502 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -12,7 +12,6 @@ RSpec.configure do |config| config.shared_context_metadata_behavior = :apply_to_host_groups config.disable_monkey_patching! config.expose_dsl_globally = true - config.warnings = true config.profile_examples = 10 config.order = :random Kernel.srand config.seed diff --git a/rubocop/cop/migration/update_column_in_batches.rb b/rubocop/cop/migration/update_column_in_batches.rb new file mode 100644 index 0000000000000000000000000000000000000000..3f886cbfea38c15cc4d11d06d872b93170e50ce4 --- /dev/null +++ b/rubocop/cop/migration/update_column_in_batches.rb @@ -0,0 +1,43 @@ +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module Migration + # Cop that checks if a spec file exists for any migration using + # `update_column_in_batches`. + class UpdateColumnInBatches < RuboCop::Cop::Cop + include MigrationHelpers + + MSG = 'Migration running `update_column_in_batches` must have a spec file at' \ + ' `%s`.'.freeze + + def on_send(node) + return unless in_migration?(node) + return unless node.children[1] == :update_column_in_batches + + spec_path = spec_filename(node) + + unless File.exist?(File.expand_path(spec_path, rails_root)) + add_offense(node, :expression, format(MSG, spec_path)) + end + end + + private + + def spec_filename(node) + source_name = node.location.expression.source_buffer.name + path = Pathname.new(source_name).relative_path_from(rails_root) + dirname = File.dirname(path) + .sub(%r{\Adb/(migrate|post_migrate)}, 'spec/migrations') + filename = File.basename(source_name, '.rb').sub(%r{\A\d+_}, '') + + File.join(dirname, "#{filename}_spec.rb") + end + + def rails_root + Pathname.new(File.expand_path('../../..', __dir__)) + end + end + end + end +end diff --git a/rubocop/migration_helpers.rb b/rubocop/migration_helpers.rb index 3160a784a042299eb5cc450ca09cd859eef96a36..c3473771178ca465594993594933c931bde1fca9 100644 --- a/rubocop/migration_helpers.rb +++ b/rubocop/migration_helpers.rb @@ -3,8 +3,9 @@ module RuboCop module MigrationHelpers # Returns true if the given node originated from the db/migrate directory. def in_migration?(node) - File.dirname(node.location.expression.source_buffer.name). - end_with?('db/migrate') + dirname = File.dirname(node.location.expression.source_buffer.name) + + dirname.end_with?('db/migrate', 'db/post_migrate') end end end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index 4ff204f939e56d236e519ebcc02fa569af8b34a1..b65efbc41f4de71a725201b9fd5993b0a4cf0cc4 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -8,3 +8,4 @@ require_relative 'cop/migration/add_index' require_relative 'cop/migration/remove_concurrent_index' require_relative 'cop/migration/remove_index' require_relative 'cop/migration/reversible_add_column_with_default' +require_relative 'cop/migration/update_column_in_batches' diff --git a/scripts/trigger-build b/scripts/trigger-build index 565bc314ef1266b3e9501a84bbf64ba0d0e41e9b..e4603533872d19694cc3a0c547ce75b99f617fc2 100755 --- a/scripts/trigger-build +++ b/scripts/trigger-build @@ -8,7 +8,8 @@ params = { "ref" => ENV["OMNIBUS_BRANCH"] || "master", "token" => ENV["BUILD_TRIGGER_TOKEN"], "variables[GITLAB_VERSION]" => ENV["CI_COMMIT_SHA"], - "variables[ALTERNATIVE_SOURCES]" => true + "variables[ALTERNATIVE_SOURCES]" => true, + "variables[ee]" => ENV["EE_PACKAGE"] } Dir.glob("*_VERSION").each do |version_file| diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 7d2f6dd9d0a5f0eadb682441f1ad87afb1247ab7..2c9d1ffc9c29108d89963600d4c15a6fea736bf8 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -22,7 +22,7 @@ describe AutocompleteController do let(:body) { JSON.parse(response.body) } it { expect(body).to be_kind_of(Array) } - it { expect(body.size).to eq 1 } + it { expect(body.size).to eq 2 } it { expect(body.map { |u| u["username"] }).to include(user.username) } end @@ -80,8 +80,8 @@ describe AutocompleteController do end it { expect(body).to be_kind_of(Array) } - it { expect(body.size).to eq 2 } - it { expect(body.map { |u| u['username'] }).to match_array([user.username, non_member.username]) } + it { expect(body.size).to eq 3 } + it { expect(body.map { |u| u['username'] }).to include(user.username, non_member.username) } end end @@ -97,6 +97,20 @@ describe AutocompleteController do it { expect(body.size).to eq User.count } end + context 'limited users per page' do + let(:per_page) { 2 } + + before do + sign_in(user) + get(:users, per_page: per_page) + end + + let(:body) { JSON.parse(response.body) } + + it { expect(body).to be_kind_of(Array) } + it { expect(body.size).to eq per_page } + end + context 'unauthenticated user' do let(:public_project) { create(:project, :public) } let(:body) { JSON.parse(response.body) } @@ -108,7 +122,7 @@ describe AutocompleteController do end it { expect(body).to be_kind_of(Array) } - it { expect(body.size).to eq 1 } + it { expect(body.size).to eq 2 } end describe 'GET #users with project' do diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 4626f1ebc29718af6bcdfeeb12f69133a51dc114..b0b24b1de1bcdd67ed1429d342c258c6639457c9 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -26,7 +26,7 @@ describe GroupsController do end end - describe 'GET #subgroups' do + describe 'GET #subgroups', :nested_groups do let!(:public_subgroup) { create(:group, :public, parent: group) } let!(:private_subgroup) { create(:group, :private, parent: group) } diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 010e3180ea4278577d92284a18ec1a5644c48151..0be7bc6a0452cd7fb9e9b8882577ceb38c7bf50c 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -133,9 +133,13 @@ describe Import::BitbucketController do end context "when a namespace with the Bitbucket user's username already exists" do - let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } + let!(:existing_namespace) { create(:group, name: other_username) } context "when the namespace is owned by the GitLab user" do + before do + existing_namespace.add_owner(user) + end + it "takes the existing namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). to receive(:new).with(bitbucket_repo, bitbucket_repo.name, existing_namespace, user, access_params). @@ -146,11 +150,6 @@ describe Import::BitbucketController do end context "when the namespace is not owned by the GitLab user" do - before do - existing_namespace.owner = create(:user) - existing_namespace.save - end - it "doesn't create a project" do expect(Gitlab::BitbucketImport::ProjectCreator). not_to receive(:new) @@ -202,10 +201,14 @@ describe Import::BitbucketController do end context 'user has chosen an existing nested namespace and name for the project' do - let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) } - let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) } + let(:parent_namespace) { create(:group, name: 'foo', owner: user) } + let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) } let(:test_name) { 'test_name' } + before do + nested_namespace.add_owner(user) + end + it 'takes the selected namespace and name' do expect(Gitlab::BitbucketImport::ProjectCreator). to receive(:new).with(bitbucket_repo, test_name, nested_namespace, user, access_params). @@ -248,7 +251,7 @@ describe Import::BitbucketController do context 'user has chosen existent and non-existent nested namespaces and name for the project' do let(:test_name) { 'test_name' } - let!(:parent_namespace) { create(:namespace, name: 'foo', owner: user) } + let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } it 'takes the selected namespace and name' do expect(Gitlab::BitbucketImport::ProjectCreator). diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index 3270ea059fa7d86b9f944f0d7979181cfbbcb6c8..3afd09063d7efc6674ec0c83252e45f3eecf2cda 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -108,9 +108,13 @@ describe Import::GitlabController do end context "when a namespace with the GitLab.com user's username already exists" do - let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } + let!(:existing_namespace) { create(:group, name: other_username) } context "when the namespace is owned by the GitLab server user" do + before do + existing_namespace.add_owner(user) + end + it "takes the existing namespace" do expect(Gitlab::GitlabImport::ProjectCreator). to receive(:new).with(gitlab_repo, existing_namespace, user, access_params). @@ -121,11 +125,6 @@ describe Import::GitlabController do end context "when the namespace is not owned by the GitLab server user" do - before do - existing_namespace.owner = create(:user) - existing_namespace.save - end - it "doesn't create a project" do expect(Gitlab::GitlabImport::ProjectCreator). not_to receive(:new) @@ -176,8 +175,12 @@ describe Import::GitlabController do end context 'user has chosen an existing nested namespace for the project' do - let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) } - let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) } + let(:parent_namespace) { create(:group, name: 'foo', owner: user) } + let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) } + + before do + nested_namespace.add_owner(user) + end it 'takes the selected namespace and name' do expect(Gitlab::GitlabImport::ProjectCreator). @@ -221,7 +224,7 @@ describe Import::GitlabController do context 'user has chosen existent and non-existent nested namespaces and name for the project' do let(:test_name) { 'test_name' } - let!(:parent_namespace) { create(:namespace, name: 'foo', owner: user) } + let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } it 'takes the selected namespace and name' do expect(Gitlab::GitlabImport::ProjectCreator). diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 587a5820c6f81aff5d05478a6c94028d9025333c..08024a2148b4dce8380d0922897a6c4af3e9bfef 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Projects::MergeRequestsController do let(:project) { create(:project) } - let(:user) { create(:user) } + let(:user) { project.owner } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } let(:merge_request_with_conflicts) do create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr| @@ -12,7 +12,6 @@ describe Projects::MergeRequestsController do before do sign_in(user) - project.team << [user, :master] end describe 'GET new' do @@ -304,6 +303,8 @@ describe Projects::MergeRequestsController do end context 'when user cannot access' do + let(:user) { create(:user) } + before do project.add_reporter(user) xhr :post, :merge, base_params @@ -459,6 +460,8 @@ describe Projects::MergeRequestsController do end describe "DELETE destroy" do + let(:user) { create(:user) } + it "denies access to users unless they're admin or project owner" do delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 7a76f5f8afce0fc88a0ba63a96535ea8dd0ef3e6..e8a9b688319717e49053faf4af269633cefc8d21 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -109,6 +109,18 @@ FactoryGirl.define do merge_requests_access_level: merge_requests_access_level, repository_access_level: evaluator.repository_access_level ) + + # Normally the class Projects::CreateService is used for creating + # projects, and this class takes care of making sure the owner and current + # user have access to the project. Our specs don't use said service class, + # thus we must manually refresh things here. + owner = project.owner + + if owner && owner.is_a?(User) && !project.pending_delete + project.members.create!(user: owner, access_level: Gitlab::Access::MASTER) + end + + project.group&.refresh_members_authorized_projects end end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index 28ddd0da753ff4914fc9e207cc6983487472bc6b..3fad4d2d6588b94fd49abdb3e2013cfcd7999e2a 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -20,7 +20,6 @@ FactoryGirl.define do project factory: :empty_project active true properties({ - namespace: 'somepath', api_url: 'https://kubernetes.example.com', token: 'a' * 40 }) diff --git a/spec/factories/web_hook_log.rb b/spec/factories/web_hook_log.rb new file mode 100644 index 0000000000000000000000000000000000000000..230b3f6b26e35ec21efe1fe03e0dfba7709c6e8f --- /dev/null +++ b/spec/factories/web_hook_log.rb @@ -0,0 +1,14 @@ +FactoryGirl.define do + factory :web_hook_log do + web_hook factory: :project_hook + trigger 'push_hooks' + url { generate(:url) } + request_headers {} + request_data {} + response_headers {} + response_body '' + response_status '200' + execution_duration 2.0 + internal_error_message nil + end +end diff --git a/spec/features/admin/admin_hook_logs_spec.rb b/spec/features/admin/admin_hook_logs_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5b67f4de6ac309aed016dc5254d5b32cdc155165 --- /dev/null +++ b/spec/features/admin/admin_hook_logs_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +feature 'Admin::HookLogs', feature: true do + let(:project) { create(:project) } + let(:system_hook) { create(:system_hook) } + let(:hook_log) { create(:web_hook_log, web_hook: system_hook, internal_error_message: 'some error') } + + before do + login_as :admin + end + + scenario 'show list of hook logs' do + hook_log + visit edit_admin_hook_path(system_hook) + + expect(page).to have_content('Recent Deliveries') + expect(page).to have_content(hook_log.url) + end + + scenario 'show hook log details' do + hook_log + visit edit_admin_hook_path(system_hook) + click_link 'View details' + + expect(page).to have_content("POST #{hook_log.url}") + expect(page).to have_content(hook_log.internal_error_message) + expect(page).to have_content('Resend Request') + end + + scenario 'retry hook log' do + WebMock.stub_request(:post, system_hook.url) + + hook_log + visit edit_admin_hook_path(system_hook) + click_link 'View details' + click_link 'Resend Request' + + expect(current_path).to eq(edit_admin_hook_path(system_hook)) + end +end diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index c5f24d412d7802a090f9efcf67962f898a6a0f04..80f7ec43c060c76f2a31ae7f424c065f019a6313 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -58,10 +58,19 @@ describe 'Admin::Hooks', feature: true do end describe 'Remove existing hook' do - it 'remove existing hook' do - visit admin_hooks_path + context 'removes existing hook' do + it 'from hooks list page' do + visit admin_hooks_path + + expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1) + end - expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1) + it 'from hook edit page' do + visit admin_hooks_path + click_link 'Edit' + + expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1) + end end end diff --git a/spec/features/admin/admin_system_info_spec.rb b/spec/features/admin/admin_system_info_spec.rb index 1df972843e2e9c3e678b943b48ec1f006d2bc98c..1548234788668c6df091677f0329c52a0be32b0e 100644 --- a/spec/features/admin/admin_system_info_spec.rb +++ b/spec/features/admin/admin_system_info_spec.rb @@ -20,6 +20,7 @@ describe 'Admin System Info' do expect(page).to have_content 'CPU 2 cores' expect(page).to have_content 'Memory 4 GB / 16 GB' expect(page).to have_content 'Disks' + expect(page).to have_content 'Uptime' end end @@ -34,6 +35,7 @@ describe 'Admin System Info' do expect(page).to have_content 'CPU Unable to collect CPU info' expect(page).to have_content 'Memory 4 GB / 16 GB' expect(page).to have_content 'Disks' + expect(page).to have_content 'Uptime' end end @@ -48,6 +50,7 @@ describe 'Admin System Info' do expect(page).to have_content 'CPU 2 cores' expect(page).to have_content 'Memory Unable to collect memory info' expect(page).to have_content 'Disks' + expect(page).to have_content 'Uptime' end end end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 01351548a99e67a8a9594a94fb35b319594cd333..fa3435ab719156fa403ff9af62ab7d7e30b52c75 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -3,10 +3,11 @@ require 'spec_helper' RSpec.describe 'Dashboard Projects', feature: true do let(:user) { create(:user) } let(:project) { create(:project, name: "awesome stuff") } + let(:project2) { create(:project, :public, name: 'Community project') } before do project.team << [user, :developer] - login_as user + login_as(user) end it 'shows the project the user in a member of in the list' do @@ -14,6 +15,17 @@ RSpec.describe 'Dashboard Projects', feature: true do expect(page).to have_content('awesome stuff') end + context 'when on Starred projects tab' do + it 'shows only starred projects' do + user.toggle_star(project2) + + visit(starred_dashboard_projects_path) + + expect(page).not_to have_content(project.name) + expect(page).to have_content(project2.name) + end + end + describe "with a pipeline", redis: true do let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) } diff --git a/spec/features/groups/group_name_toggle_spec.rb b/spec/features/groups/group_name_toggle_spec.rb index 8a1d415c4f1fdb2718fa7658edf97f10a9e3e0f7..dfc3c84f29af23c0d3489f4e25deed2069a99d49 100644 --- a/spec/features/groups/group_name_toggle_spec.rb +++ b/spec/features/groups/group_name_toggle_spec.rb @@ -22,7 +22,7 @@ feature 'Group name toggle', feature: true, js: true do expect(page).not_to have_css('.group-name-toggle') end - it 'is present if the title is longer than the container' do + it 'is present if the title is longer than the container', :nested_groups do visit group_path(nested_group_3) title_width = page.evaluate_script("$('.title')[0].offsetWidth") @@ -35,7 +35,7 @@ feature 'Group name toggle', feature: true, js: true do expect(title_width).to be > container_width end - it 'should show the full group namespace when toggled' do + it 'should show the full group namespace when toggled', :nested_groups do page_height = page.current_window.size[1] page.current_window.resize_to(SMALL_SCREEN, page_height) visit group_path(nested_group_3) diff --git a/spec/features/groups/members/list_spec.rb b/spec/features/groups/members/list_spec.rb index 543879bd21da334075fd67738b6809ea6cdf0096..f654fa16a0670bc861c913eed996379146609c54 100644 --- a/spec/features/groups/members/list_spec.rb +++ b/spec/features/groups/members/list_spec.rb @@ -12,7 +12,7 @@ feature 'Groups members list', feature: true do login_as(user1) end - scenario 'show members from current group and parent' do + scenario 'show members from current group and parent', :nested_groups do group.add_developer(user1) nested_group.add_developer(user2) @@ -22,7 +22,7 @@ feature 'Groups members list', feature: true do expect(second_row.text).to include(user2.name) end - scenario 'show user once if member of both current group and parent' do + scenario 'show user once if member of both current group and parent', :nested_groups do group.add_developer(user1) nested_group.add_developer(user1) diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 3d32c47bf09afe1381dd3a75bdad12e6be4524d6..24ea7aba0ccecedfebc36fad348b2c2605111e12 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -83,7 +83,7 @@ feature 'Group', feature: true do end end - describe 'create a nested group', js: true do + describe 'create a nested group', :nested_groups, js: true do let(:group) { create(:group, path: 'foo') } context 'as admin' do @@ -196,7 +196,7 @@ feature 'Group', feature: true do end end - describe 'group page with nested groups', js: true do + describe 'group page with nested groups', :nested_groups, js: true do let!(:group) { create(:group) } let!(:nested_group) { create(:group, parent: group) } let!(:path) { group_path(group) } diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index 0b573d7cef45b9e04b341453753782f2b52ce366..4d38df05928224b0b4ce6f4d8c79c00d4004e9f4 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -58,7 +58,7 @@ describe 'Dropdown assignee', :feature, :js do it 'should load all the assignees when opened' do filtered_search.set('assignee:') - expect(dropdown_assignee_size).to eq(3) + expect(dropdown_assignee_size).to eq(4) end it 'shows current user at top of dropdown' do diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index b29177bed0669384ae4f09ab8c3e95d9d0efe6f1..358b244fb5b16015f5f54f92b425cbbc0b5014b8 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -65,7 +65,7 @@ describe 'Dropdown author', js: true, feature: true do it 'should load all the authors when opened' do send_keys_to_filtered_search('author:') - expect(dropdown_author_size).to eq(3) + expect(dropdown_author_size).to eq(4) end it 'shows current user at top of dropdown' do diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 65d854d0896bcfdd53c1c5ed92ec1b4044c6569d..8949dbcb663bf6eaa07a51f3a31b742c723f0fd7 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -3,6 +3,7 @@ require 'rails_helper' describe 'New/edit issue', :feature, :js do include GitlabRoutingHelper include ActionView::Helpers::JavaScriptHelper + include FormHelper let!(:project) { create(:project) } let!(:user) { create(:user)} @@ -23,6 +24,65 @@ describe 'New/edit issue', :feature, :js do visit new_namespace_project_issue_path(project.namespace, project) end + describe 'shorten users API pagination limit' do + before do + allow_any_instance_of(FormHelper).to receive(:issue_dropdown_options).and_wrap_original do |original, *args| + has_multiple_assignees = *args[1] + + options = { + toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data', + title: 'Select assignee', + filter: true, + dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee', + placeholder: 'Search users', + data: { + per_page: 1, + null_user: true, + current_user: true, + project_id: project.try(:id), + field_name: "issue[assignee_ids][]", + default_label: 'Assignee', + 'max-select': 1, + 'dropdown-header': 'Assignee', + multi_select: true, + 'input-meta': 'name', + 'always-show-selectbox': true + } + } + + if has_multiple_assignees + options[:title] = 'Select assignee(s)' + options[:data][:'dropdown-header'] = 'Assignee(s)' + options[:data].delete(:'max-select') + end + + options + end + + visit new_namespace_project_issue_path(project.namespace, project) + + click_button 'Unassigned' + + wait_for_requests + end + + it 'should display selected users even if they are not part of the original API call' do + find('.dropdown-input-field').native.send_keys user2.name + + page.within '.dropdown-menu-user' do + expect(page).to have_content user2.name + click_link user2.name + end + + find('.js-dropdown-input-clear').click + + page.within '.dropdown-menu-user' do + expect(page).to have_content user.name + expect(find('.dropdown-menu-user a.is-active').first(:xpath, '..')['data-user-id']).to eq(user2.id.to_s) + end + end + end + describe 'single assignee' do before do click_button 'Unassigned' @@ -219,6 +279,37 @@ describe 'New/edit issue', :feature, :js do end end + describe 'sub-group project' do + let(:group) { create(:group) } + let(:nested_group_1) { create(:group, parent: group) } + let(:sub_group_project) { create(:empty_project, group: nested_group_1) } + + before do + sub_group_project.add_master(user) + + visit new_namespace_project_issue_path(sub_group_project.namespace, sub_group_project) + end + + it 'creates new label from dropdown' do + click_button 'Labels' + + click_link 'Create new label' + + page.within '.dropdown-new-label' do + fill_in 'new_label_name', with: 'test label' + first('.suggest-colors-dropdown a').click + + click_button 'Create' + + wait_for_requests + end + + page.within '.dropdown-menu-labels' do + expect(page).to have_link 'test label' + end + end + end + def before_for_selector(selector) js = <<-JS.strip_heredoc (function(selector) { diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 99ad8013023ac48fd454d5c22e8355b17dba2f66..96c24750250a7036a2709e8af80eef20c6ece7fe 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -57,6 +57,23 @@ feature 'Issue Sidebar', feature: true do expect(page.find('.dropdown-menu-user-link.is-active')).to have_content(user.name) end end + + it 'keeps your filtered term after filtering and dismissing the dropdown' do + find('.dropdown-input-field').native.send_keys user2.name + + wait_for_requests + + page.within '.dropdown-menu-user' do + expect(page).not_to have_content 'Unassigned' + click_link user2.name + end + + find('.js-right-sidebar').click + find('.block.assignee .edit-link').click + + expect(page.all('.dropdown-menu-user li').length).to eq(1) + expect(find('.dropdown-input-field').value).to eq(user2.name) + end end context 'as a allowed user' do diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb index ec87a99b3ab0c4aeb2b4a036a0220be5f0f88168..c77a5c68bc68d096b1186663303aecf80f856a7c 100644 --- a/spec/features/merge_requests/edit_mr_spec.rb +++ b/spec/features/merge_requests/edit_mr_spec.rb @@ -29,6 +29,19 @@ feature 'Edit Merge Request', feature: true do expect(page).to have_content 'Someone edited the merge request the same time you did' end + it 'allows to unselect "Remove source branch"', js: true do + merge_request.update(merge_params: { 'force_remove_source_branch' => '1' }) + expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy + + visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request) + uncheck 'Remove source branch when merge request is accepted' + + click_button 'Save changes' + + expect(page).to have_unchecked_field 'remove-source-branch-input' + expect(page).to have_content 'Remove source branch' + end + it 'should preserve description textarea height', js: true do long_description = %q( Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ac ornare ligula, ut tempus arcu. Etiam ultricies accumsan dolor vitae faucibus. Donec at elit lacus. Mauris orci ante, aliquam quis lorem eget, convallis faucibus arcu. Aenean at pulvinar lacus. Ut viverra quam massa, molestie ornare tortor dignissim a. Suspendisse tristique pellentesque tellus, id lacinia metus elementum id. Nam tristique, arcu rhoncus faucibus viverra, lacus ipsum sagittis ligula, vitae convallis odio lacus a nibh. Ut tincidunt est purus, ac vestibulum augue maximus in. Suspendisse vel erat et mi ultricies semper. Pellentesque volutpat pellentesque consequat. diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb index e08721b4724af213c0a32ad15ee2a2d9824469e8..09f889d4dd6b71b7f08d2ac6f772e7e35c0f2b7f 100644 --- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb @@ -7,7 +7,8 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do let(:merge_request) do create(:merge_request_with_diffs, source_project: project, author: user, - title: 'Bug NS-04') + title: 'Bug NS-04', + merge_params: { force_remove_source_branch: '1' }) end let(:pipeline) do @@ -41,7 +42,7 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do click_button "Merge when pipeline succeeds" expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds." - expect(page).to have_content "The source branch will be removed." + expect(page).to have_content "The source branch will not be removed." expect(page).to have_selector ".js-cancel-auto-merge" visit_merge_request(merge_request) # Needed to refresh the page expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i @@ -82,7 +83,8 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do source_project: project, title: 'Bug NS-04', author: user, - merge_user: user) + merge_user: user, + merge_params: { force_remove_source_branch: '1' }) end before do @@ -99,7 +101,7 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do click_link 'Merge when pipeline succeeds' expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds." - expect(page).to have_content "The source branch will be removed." + expect(page).to have_content "The source branch will not be removed." expect(page).to have_link "Cancel automatic merge" end end diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index be8b1423c20299623ec415e8a45835ac99d9aba0..4f3a5119915538553bc755b8c693127ec7d427c5 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -202,4 +202,25 @@ describe 'Merge request', :feature, :js do end end end + + context 'user can merge into source project but cannot push to fork', js: true do + let(:fork_project) { create(:project, :public) } + let(:user2) { create(:user) } + + before do + project.team << [user2, :master] + logout + login_as user2 + merge_request.update(target_project: fork_project) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'user can merge into the source project' do + expect(page).to have_button('Merge', disabled: false) + end + + it 'user cannot remove source branch' do + expect(page).to have_field('remove-source-branch-input', disabled: true) + end + end end diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb index c969acc9140482285e432ee0da3a1363ba025c8d..4e5682c8636c15dad2e9faffe6a337254bc0202f 100644 --- a/spec/features/projects/group_links_spec.rb +++ b/spec/features/projects/group_links_spec.rb @@ -40,7 +40,7 @@ feature 'Project group links', :feature, :js do another_group.add_master(master) end - it 'does not show ancestors' do + it 'does not show ancestors', :nested_groups do visit namespace_project_settings_members_path(project.namespace, project) click_link 'Search for a group' diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb index b7ae5f0b9258af4ca520aeaf320f7a9424c2e0c3..d428f6fcf2237941810e8228d1b7f229ad5cd95f 100644 --- a/spec/features/projects/members/sorting_spec.rb +++ b/spec/features/projects/members/sorting_spec.rb @@ -3,10 +3,9 @@ require 'spec_helper' feature 'Projects > Members > Sorting', feature: true do let(:master) { create(:user, name: 'John Doe') } let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) } - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, namespace: master.namespace, creator: master) } background do - create(:project_member, :master, user: master, project: project, created_at: 5.days.ago) create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago) login_as(master) @@ -39,16 +38,16 @@ feature 'Projects > Members > Sorting', feature: true do scenario 'sorts by last joined' do visit_members_list(sort: :last_joined) - expect(first_member).to include(developer.name) - expect(second_member).to include(master.name) + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined') end scenario 'sorts by oldest joined' do visit_members_list(sort: :oldest_joined) - expect(first_member).to include(master.name) - expect(second_member).to include(developer.name) + expect(first_member).to include(developer.name) + expect(second_member).to include(master.name) expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined') end diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb index 1bf8f710b9fe6ee01cb5c015c6c24b2d37f96159..ec48a4bd726ab29114780ff4285419ecd767a876 100644 --- a/spec/features/projects/members/user_requests_access_spec.rb +++ b/spec/features/projects/members/user_requests_access_spec.rb @@ -2,11 +2,10 @@ require 'spec_helper' feature 'Projects > Members > User requests access', feature: true do let(:user) { create(:user) } - let(:master) { create(:user) } let(:project) { create(:project, :public, :access_requestable) } + let(:master) { project.owner } background do - project.team << [master, :master] login_as(user) visit namespace_project_path(project.namespace, project) end diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index f40e1bc49306cebc26b93244ed6c54a9b18e84cc..317949d6b565059189d29d86262bebd7695f6ebf 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -65,6 +65,17 @@ feature 'Pipeline Schedules', :feature do expect(page).not_to have_content('pipeline schedule') end end + + context 'when ref is nil' do + before do + pipeline_schedule.update_attribute(:ref, nil) + visit_pipelines_schedules + end + + it 'shows a list of the pipeline schedules with empty ref column' do + expect(first('.branch-name-cell').text).to eq('') + end + end end describe 'POST /projects/pipeline_schedules/new', js: true do @@ -108,6 +119,19 @@ feature 'Pipeline Schedules', :feature do expect(page).to have_content('my brand new description') end + + context 'when ref is nil' do + before do + pipeline_schedule.update_attribute(:ref, nil) + edit_pipeline_schedule + end + + it 'shows the pipeline schedule with default ref' do + page.within('.git-revision-dropdown-toggle') do + expect(first('.dropdown-toggle-text').text).to eq('master') + end + end + end end def visit_new_pipeline_schedule diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb index d3232f0cc160d19c88c2613d66e6382b1c69c3b7..fbaea14a2bedd46a15794dd8c75c728e92c4591b 100644 --- a/spec/features/projects/settings/integration_settings_spec.rb +++ b/spec/features/projects/settings/integration_settings_spec.rb @@ -85,11 +85,55 @@ feature 'Integration settings', feature: true do expect(current_path).to eq(integrations_path) end - scenario 'remove existing webhook' do - hook - visit integrations_path + context 'remove existing webhook' do + scenario 'from webhooks list page' do + hook + visit integrations_path + + expect { click_link 'Remove' }.to change(ProjectHook, :count).by(-1) + end + + scenario 'from webhook edit page' do + hook + visit integrations_path + click_link 'Edit' + + expect { click_link 'Remove' }.to change(ProjectHook, :count).by(-1) + end + end + end + + context 'Webhook logs' do + let(:hook) { create(:project_hook, project: project) } + let(:hook_log) { create(:web_hook_log, web_hook: hook, internal_error_message: 'some error') } + + scenario 'show list of hook logs' do + hook_log + visit edit_namespace_project_hook_path(project.namespace, project, hook) + + expect(page).to have_content('Recent Deliveries') + expect(page).to have_content(hook_log.url) + end + + scenario 'show hook log details' do + hook_log + visit edit_namespace_project_hook_path(project.namespace, project, hook) + click_link 'View details' + + expect(page).to have_content("POST #{hook_log.url}") + expect(page).to have_content(hook_log.internal_error_message) + expect(page).to have_content('Resend Request') + end + + scenario 'retry hook log' do + WebMock.stub_request(:post, hook.url) + + hook_log + visit edit_namespace_project_hook_path(project.namespace, project, hook) + click_link 'View details' + click_link 'Resend Request' - expect { click_link 'Remove' }.to change(ProjectHook, :count).by(-1) + expect(current_path).to eq(edit_namespace_project_hook_path(project.namespace, project, hook)) end end end diff --git a/spec/features/projects/sub_group_issuables_spec.rb b/spec/features/projects/sub_group_issuables_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..cf21b208f65e951482607fe3fbbbe28cda7c85b4 --- /dev/null +++ b/spec/features/projects/sub_group_issuables_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe 'Subgroup Issuables', :feature, :js do + let!(:group) { create(:group, name: 'group') } + let!(:subgroup) { create(:group, parent: group, name: 'subgroup') } + let!(:project) { create(:empty_project, namespace: subgroup, name: 'project') } + let(:user) { create(:user) } + + before do + project.add_master(user) + login_as user + end + + it 'shows the full subgroup title when issues index page is empty' do + visit namespace_project_issues_path(project.namespace.to_param, project.to_param) + + expect_to_have_full_subgroup_title + end + + it 'shows the full subgroup title when merge requests index page is empty' do + visit namespace_project_merge_requests_path(project.namespace.to_param, project.to_param) + + expect_to_have_full_subgroup_title + end + + def expect_to_have_full_subgroup_title + title = find('.title-container') + + expect(title).not_to have_selector '.initializing' + expect(title).to have_content 'group / subgroup / project' + end +end diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb index b762756f9ce2358a5de4fbc5f1b581e396d1d61d..db3fcc2347563825f3c1250d484539689fd3bdcd 100644 --- a/spec/finders/group_members_finder_spec.rb +++ b/spec/finders/group_members_finder_spec.rb @@ -18,7 +18,7 @@ describe GroupMembersFinder, '#execute' do expect(result.to_a).to eq([member3, member2, member1]) end - it 'returns members for nested group' do + it 'returns members for nested group', :nested_groups do group.add_master(user2) nested_group.request_access(user4) member1 = group.add_master(user1) diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb index cf691cf684b72351ca00be301c1001cfa0c8fefe..300ba8422e883d572647f6609407f2c4157bb12e 100644 --- a/spec/finders/members_finder_spec.rb +++ b/spec/finders/members_finder_spec.rb @@ -9,7 +9,7 @@ describe MembersFinder, '#execute' do let(:user3) { create(:user) } let(:user4) { create(:user) } - it 'returns members for project and parent groups' do + it 'returns members for project and parent groups', :nested_groups do nested_group.request_access(user1) member1 = group.add_master(user2) member2 = nested_group.add_master(user3) diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json index 4afbb87453e07daa4d7a8241ad0a22eac4a6a989..b6a59a6cc4739732b22008ef20737a006dbe1d79 100644 --- a/spec/fixtures/api/schemas/entities/merge_request.json +++ b/spec/fixtures/api/schemas/entities/merge_request.json @@ -92,7 +92,8 @@ "diverged_commits_count": { "type": "integer" }, "commit_change_content_path": { "type": "string" }, "remove_wip_path": { "type": "string" }, - "commits_count": { "type": "integer" } + "commits_count": { "type": "integer" }, + "remove_source_branch": { "type": ["boolean", "null"] } }, "additionalProperties": false } diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb index 18935be95c99fce290608f199020ea2450086fde..b05ae5c223236cf56ee720f98803e4c5db4f56b8 100644 --- a/spec/helpers/submodule_helper_spec.rb +++ b/spec/helpers/submodule_helper_spec.rb @@ -115,6 +115,11 @@ describe SubmoduleHelper do expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash']) end + it 'handles urls with trailing whitespace' do + stub_url('http://gitlab.com/gitlab-org/gitlab-ce.git ') + expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash']) + end + it 'returns original with non-standard url' do stub_url('http://gitlab.com/another/gitlab-org/gitlab-ce.git') expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil]) diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js index 90b12c9f1153776fd21c3e15fb5676cea3163abc..83c92deccdceeaf488232b6e927eecdb548e81b0 100644 --- a/spec/javascripts/lib/utils/number_utility_spec.js +++ b/spec/javascripts/lib/utils/number_utility_spec.js @@ -1,4 +1,4 @@ -import { formatRelevantDigits, bytesToKiB } from '~/lib/utils/number_utils'; +import { formatRelevantDigits, bytesToKiB, bytesToMiB } from '~/lib/utils/number_utils'; describe('Number Utils', () => { describe('formatRelevantDigits', () => { @@ -45,4 +45,11 @@ describe('Number Utils', () => { expect(bytesToKiB(1000)).toEqual(0.9765625); }); }); + + describe('bytesToMiB', () => { + it('calculates MiB for the given bytes', () => { + expect(bytesToMiB(1048576)).toEqual(1); + expect(bytesToMiB(1000000)).toEqual(0.95367431640625); + }); + }); }); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 3d1706aab686ae2a9151123e08e3283b1d91bdf1..7b910282cc88d62fa9cafe2d4ec988c0dcba7046 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,4 +1,5 @@ /* eslint-disable no-var, comma-dangle, object-shorthand */ +/* global Notes */ import '~/merge_request_tabs'; import '~/commit/pipelines/pipelines_bundle'; @@ -7,6 +8,7 @@ import '~/lib/utils/common_utils'; import '~/diff'; import '~/single_file_diff'; import '~/files_comment_button'; +import '~/notes'; import 'vendor/jquery.scrollTo'; (function () { @@ -29,7 +31,7 @@ import 'vendor/jquery.scrollTo'; }; $.extend(stubLocation, defaults, stubs || {}); }; - preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + preloadFixtures('merge_requests/merge_request_with_task_list.html.raw', 'merge_requests/diff_comment.html.raw'); beforeEach(function () { this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation }); @@ -286,8 +288,49 @@ import 'vendor/jquery.scrollTo'; spyOn($, 'ajax').and.callFake(function (options) { expect(options.url).toEqual('/foo/bar/merge_requests/1/diffs.json'); }); + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); }); + + describe('with note fragment hash', () => { + beforeEach(() => { + loadFixtures('merge_requests/diff_comment.html.raw'); + spyOn(window.gl.utils, 'getPagePath').and.returnValue('merge_requests'); + window.notes = new Notes('', []); + spyOn(window.notes, 'toggleDiffNote').and.callThrough(); + }); + + afterEach(() => { + delete window.notes; + }); + + it('should expand and scroll to linked fragment hash #note_xxx', function () { + const noteId = 'note_1'; + spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId); + spyOn($, 'ajax').and.callFake(function (options) { + options.success({ html: `<div id="${noteId}">foo</div>` }); + }); + + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + + expect(window.notes.toggleDiffNote).toHaveBeenCalledWith({ + target: jasmine.any(Object), + lineType: 'old', + forceShow: true, + }); + }); + + it('should gracefully ignore non-existant fragment hash', function () { + spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); + spyOn($, 'ajax').and.callFake(function (options) { + options.success({ html: '' }); + }); + + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + + expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); + }); + }); }); }); }).call(window); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 025f08ee3323379c1fff484e205ea5e921e304ca..04cf0fe2bf8fb7c52047d08cb88122b7dcefe637 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -128,7 +128,6 @@ import '~/notes'; beforeEach(() => { note = { id: 1, - discussion_html: null, valid: true, note: 'heya', html: '<div>heya</div>', diff --git a/spec/javascripts/pipelines/graph/graph_component_spec.js b/spec/javascripts/pipelines/graph/graph_component_spec.js index 6bd0eb86263346dd77c9d9bb5825af15264d4d42..713baa65a172ceab7d4a32a1a6957b85e83fe92c 100644 --- a/spec/javascripts/pipelines/graph/graph_component_spec.js +++ b/spec/javascripts/pipelines/graph/graph_component_spec.js @@ -14,49 +14,42 @@ describe('graph component', () => { describe('while is loading', () => { it('should render a loading icon', () => { - const component = new GraphComponent().$mount('#js-pipeline-graph-vue'); + const component = new GraphComponent({ + propsData: { + isLoading: true, + pipeline: {}, + }, + }).$mount('#js-pipeline-graph-vue'); expect(component.$el.querySelector('.loading-icon')).toBeDefined(); }); }); - describe('with a successfull response', () => { - const interceptor = (request, next) => { - next(request.respondWith(JSON.stringify(graphJSON), { - status: 200, - })); - }; + describe('with data', () => { + it('should render the graph', () => { + const component = new GraphComponent({ + propsData: { + isLoading: false, + pipeline: graphJSON, + }, + }).$mount('#js-pipeline-graph-vue'); - beforeEach(() => { - Vue.http.interceptors.push(interceptor); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); - }); - - it('should render the graph', (done) => { - const component = new GraphComponent().$mount('#js-pipeline-graph-vue'); - - setTimeout(() => { - expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true); + expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true); - expect( - component.$el.querySelector('.stage-column:first-child').classList.contains('no-margin'), - ).toEqual(true); + expect( + component.$el.querySelector('.stage-column:first-child').classList.contains('no-margin'), + ).toEqual(true); - expect( - component.$el.querySelector('.stage-column:nth-child(2)').classList.contains('left-margin'), - ).toEqual(true); + expect( + component.$el.querySelector('.stage-column:nth-child(2)').classList.contains('left-margin'), + ).toEqual(true); - expect( - component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'), - ).toEqual(true); + expect( + component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'), + ).toEqual(true); - expect(component.$el.querySelector('loading-icon')).toBe(null); + expect(component.$el.querySelector('loading-icon')).toBe(null); - expect(component.$el.querySelector('.stage-column-list')).toBeDefined(); - done(); - }, 0); + expect(component.$el.querySelector('.stage-column-list')).toBeDefined(); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js index da9dff18ada254eed5855f88600c8839b99484d0..2c3d0ddff28ea488af30e2131fe74e77aea61dae 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js @@ -7,6 +7,18 @@ const url = '/root/acets-review-apps/environments/15/deployments/1/metrics'; const metricsMockData = { success: true, metrics: { + memory_before: [ + { + metric: {}, + value: [1495785220.607, '9572875.906976745'], + }, + ], + memory_after: [ + { + metric: {}, + value: [1495787020.607, '4485853.130206379'], + }, + ], memory_values: [ { metric: {}, @@ -39,7 +51,7 @@ const createComponent = () => { const messages = { loadingMetrics: 'Loading deployment statistics.', - hasMetrics: 'Deployment memory usage:', + hasMetrics: 'Memory usage unchanged from 0MB to 0MB', loadFailed: 'Failed to load deployment statistics.', metricsUnavailable: 'Deployment statistics are not available currently.', }; @@ -89,17 +101,52 @@ describe('MemoryUsage', () => { }); }); + describe('computed', () => { + describe('memoryChangeType', () => { + it('should return "increased" if memoryFrom value is less than memoryTo value', () => { + vm.memoryFrom = 4.28; + vm.memoryTo = 9.13; + + expect(vm.memoryChangeType).toEqual('increased'); + }); + + it('should return "decreased" if memoryFrom value is less than memoryTo value', () => { + vm.memoryFrom = 9.13; + vm.memoryTo = 4.28; + + expect(vm.memoryChangeType).toEqual('decreased'); + }); + + it('should return "unchanged" if memoryFrom value equal to memoryTo value', () => { + vm.memoryFrom = 1; + vm.memoryTo = 1; + + expect(vm.memoryChangeType).toEqual('unchanged'); + }); + }); + }); + describe('methods', () => { const { metrics, deployment_time } = metricsMockData; + describe('getMegabytes', () => { + it('should return Megabytes from provided Bytes value', () => { + const memoryInBytes = '9572875.906976745'; + + expect(vm.getMegabytes(memoryInBytes)).toEqual('9.13'); + }); + }); + describe('computeGraphData', () => { it('should populate sparkline graph', () => { vm.computeGraphData(metrics, deployment_time); - const { hasMetrics, memoryMetrics, deploymentTime } = vm; + const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm; expect(hasMetrics).toBeTruthy(); expect(memoryMetrics.length > 0).toBeTruthy(); expect(deploymentTime).toEqual(deployment_time); + expect(memoryFrom).toEqual('9.13'); + expect(memoryTo).toEqual('4.28'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index d043ad38b8bd771ce44cd3337b7d206b94242eef..732b516badd301a9c038fe1c5019dbf52f2eb175 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -5,7 +5,7 @@ import * as simplePoll from '~/lib/utils/simple_poll'; const commitMessage = 'This is the commit message'; const commitMessageWithDescription = 'This is the commit message description'; -const createComponent = () => { +const createComponent = (customConfig = {}) => { const Component = Vue.extend(readyToMergeComponent); const mr = { isPipelineActive: false, @@ -17,8 +17,12 @@ const createComponent = () => { sha: '12345678', commitMessage, commitMessageWithDescription, + shouldRemoveSourceBranch: true, + canRemoveSourceBranch: false, }; + Object.assign(mr, customConfig.mr); + const service = { merge() {}, poll() {}, @@ -51,7 +55,6 @@ describe('MRWidgetReadyToMerge', () => { describe('data', () => { it('should have default data', () => { - expect(vm.removeSourceBranch).toBeTruthy(true); expect(vm.mergeWhenBuildSucceeds).toBeFalsy(); expect(vm.useCommitMessageWithDescription).toBeFalsy(); expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy(); @@ -166,6 +169,36 @@ describe('MRWidgetReadyToMerge', () => { expect(vm.isMergeButtonDisabled).toBeTruthy(); }); }); + + describe('Remove source branch checkbox', () => { + describe('when user can merge but cannot delete branch', () => { + it('isRemoveSourceBranchButtonDisabled should be true', () => { + expect(vm.isRemoveSourceBranchButtonDisabled).toBe(true); + }); + + it('should be disabled in the rendered output', () => { + const checkboxElement = vm.$el.querySelector('#remove-source-branch-input'); + expect(checkboxElement.getAttribute('disabled')).toBe('disabled'); + }); + }); + + describe('when user can merge and can delete branch', () => { + beforeEach(() => { + this.customVm = createComponent({ + mr: { canRemoveSourceBranch: true }, + }); + }); + + it('isRemoveSourceBranchButtonDisabled should be false', () => { + expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false); + }); + + it('should be enabled in rendered output', () => { + const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input'); + expect(checkboxElement.getAttribute('disabled')).toBeNull(); + }); + }); + }); }); describe('methods', () => { diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..1bf8916b3d0ffd8c777c7a458412100bfcba2930 --- /dev/null +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -0,0 +1,82 @@ +import Vue from 'vue'; +import headerCi from '~/vue_shared/components/header_ci_component.vue'; + +describe('Header CI Component', () => { + let HeaderCi; + let vm; + let props; + + beforeEach(() => { + HeaderCi = Vue.extend(headerCi); + + props = { + status: { + group: 'failed', + icon: 'ci-status-failed', + label: 'failed', + text: 'failed', + details_path: 'path', + }, + itemName: 'job', + itemId: 123, + time: '2017-05-08T14:57:39.781Z', + user: { + web_url: 'path', + name: 'Foo', + username: 'foobar', + email: 'foo@bar.com', + avatar_url: 'link', + }, + actions: [ + { + label: 'Retry', + path: 'path', + type: 'button', + cssClass: 'btn', + }, + { + label: 'Go', + path: 'path', + type: 'link', + cssClass: 'link', + }, + ], + }; + + vm = new HeaderCi({ + propsData: props, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render status badge', () => { + expect(vm.$el.querySelector('.ci-failed')).toBeDefined(); + expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined(); + expect( + vm.$el.querySelector('.ci-failed').getAttribute('href'), + ).toEqual(props.status.details_path); + }); + + it('should render item name and id', () => { + expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123'); + }); + + it('should render timeago date', () => { + expect(vm.$el.querySelector('time')).toBeDefined(); + }); + + it('should render user icon and name', () => { + expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name); + }); + + it('should render provided actions', () => { + expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON'); + expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label); + expect(vm.$el.querySelector('.link').tagName).toEqual('A'); + expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label); + expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path); + }); +}); diff --git a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..bf28019ef241a27342409254d6451c5f5da2cc02 --- /dev/null +++ b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js @@ -0,0 +1,68 @@ +import Vue from 'vue'; +import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import '~/lib/utils/datetime_utility'; + +describe('Time ago with tooltip component', () => { + let TimeagoTooltip; + let vm; + + beforeEach(() => { + TimeagoTooltip = Vue.extend(timeagoTooltip); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render timeago with a bootstrap tooltip', () => { + vm = new TimeagoTooltip({ + propsData: { + time: '2017-05-08T14:57:39.781Z', + }, + }).$mount(); + + expect(vm.$el.tagName).toEqual('TIME'); + expect(vm.$el.classList.contains('js-timeago')).toEqual(true); + expect( + vm.$el.getAttribute('data-original-title'), + ).toEqual(gl.utils.formatDate('2017-05-08T14:57:39.781Z')); + expect(vm.$el.getAttribute('data-placement')).toEqual('top'); + + const timeago = gl.utils.getTimeago(); + + expect(vm.$el.textContent.trim()).toEqual(timeago.format('2017-05-08T14:57:39.781Z')); + }); + + it('should render tooltip placed in bottom', () => { + vm = new TimeagoTooltip({ + propsData: { + time: '2017-05-08T14:57:39.781Z', + tooltipPlacement: 'bottom', + }, + }).$mount(); + + expect(vm.$el.getAttribute('data-placement')).toEqual('bottom'); + }); + + it('should render short format class', () => { + vm = new TimeagoTooltip({ + propsData: { + time: '2017-05-08T14:57:39.781Z', + shortFormat: true, + }, + }).$mount(); + + expect(vm.$el.classList.contains('js-short-timeago')).toEqual(true); + }); + + it('should render provided html class', () => { + vm = new TimeagoTooltip({ + propsData: { + time: '2017-05-08T14:57:39.781Z', + cssClass: 'foo', + }, + }).$mount(); + + expect(vm.$el.classList.contains('foo')).toEqual(true); + }); +}); diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb index b386852b1968d8b8ef3283cbff5a6281933276b2..cfb5cba054ec0079b77d47f45ad82a96841f14fe 100644 --- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Cache::Ci::ProjectPipelineStatus, :redis do - let(:project) { create(:project) } + let!(:project) { create(:project) } let(:pipeline_status) { described_class.new(project) } let(:cache_key) { "projects/#{project.id}/pipeline_status" } @@ -18,7 +18,7 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :redis do let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' } let(:ref) { 'master' } let(:pipeline_info) { { sha: sha, status: status, ref: ref } } - let(:project_without_status) { create(:project) } + let!(:project_without_status) { create(:project) } describe '.load_in_batch_for_projects' do it 'preloads pipeline_status on projects' do diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb index 40ac5a3ed375f43ad95d3c7bd0853c507776750d..bbb3f9912a3e92401c0e86ba2af992fa7b194c1b 100644 --- a/spec/lib/gitlab/ci/trace/stream_spec.rb +++ b/spec/lib/gitlab/ci/trace/stream_spec.rb @@ -240,9 +240,50 @@ describe Gitlab::Ci::Trace::Stream do end context 'multiple results in content & regex' do - let(:data) { ' (98.39%) covered. (98.29%) covered' } + let(:data) do + <<~HEREDOC + (98.39%) covered + (98.29%) covered + HEREDOC + end + + let(:regex) { '\(\d+.\d+\%\) covered' } + + it 'returns the last matched coverage' do + is_expected.to eq("98.29") + end + end + + context 'when BUFFER_SIZE is smaller than stream.size' do + let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' } + let(:regex) { '\(\d+.\d+\%\) covered' } + + before do + stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5) + end + + it { is_expected.to eq("98.29") } + end + + context 'when regex is multi-byte char' do + let(:data) { '95.0 ゴッドファット\n' } + let(:regex) { '\d+\.\d+ ゴッドファット' } + + before do + stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5) + end + + it { is_expected.to eq('95.0') } + end + + context 'when BUFFER_SIZE is equal to stream.size' do + let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' } let(:regex) { '\(\d+.\d+\%\) covered' } + before do + stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', data.length) + end + it { is_expected.to eq("98.29") } end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index bd5ac6142be332d89cc7924594756c4d2421143a..3fdafd867da3d145d7b7d6cd3e1e1de4ceeb2ac3 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -66,16 +66,23 @@ describe Gitlab::Database::MigrationHelpers, lib: true do context 'using PostgreSQL' do before do - allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + allow(model).to receive(:supports_drop_index_concurrently?).and_return(true) allow(model).to receive(:disable_statement_timeout) end - it 'removes the index concurrently' do + it 'removes the index concurrently by column name' do expect(model).to receive(:remove_index). with(:users, { algorithm: :concurrently, column: :foo }) model.remove_concurrent_index(:users, :foo) end + + it 'removes the index concurrently by index name' do + expect(model).to receive(:remove_index). + with(:users, { algorithm: :concurrently, name: "index_x_by_y" }) + + model.remove_concurrent_index_by_name(:users, "index_x_by_y") + end end context 'using MySQL' do diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb index c56fded751697243f39ff563be4a3ca7addbc8a8..ce2b5d620fdcf464089df5235cb2c03d97e5c25c 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb @@ -18,8 +18,8 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do let(:subject) { described_class.new(['parent/the-Path'], migration) } it 'includes the namespace' do - parent = create(:namespace, path: 'parent') - child = create(:namespace, path: 'the-path', parent: parent) + parent = create(:group, path: 'parent') + child = create(:group, path: 'the-path', parent: parent) found_ids = subject.namespaces_for_paths(type: :child). map(&:id) @@ -30,13 +30,13 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do context 'for child namespaces' do it 'only returns child namespaces with the correct path' do - _root_namespace = create(:namespace, path: 'THE-path') - _other_path = create(:namespace, + _root_namespace = create(:group, path: 'THE-path') + _other_path = create(:group, path: 'other', - parent: create(:namespace)) - namespace = create(:namespace, + parent: create(:group)) + namespace = create(:group, path: 'the-path', - parent: create(:namespace)) + parent: create(:group)) found_ids = subject.namespaces_for_paths(type: :child). map(&:id) @@ -45,13 +45,13 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do end it 'has no namespaces that look the same' do - _root_namespace = create(:namespace, path: 'THE-path') - _similar_path = create(:namespace, + _root_namespace = create(:group, path: 'THE-path') + _similar_path = create(:group, path: 'not-really-the-path', - parent: create(:namespace)) - namespace = create(:namespace, + parent: create(:group)) + namespace = create(:group, path: 'the-path', - parent: create(:namespace)) + parent: create(:group)) found_ids = subject.namespaces_for_paths(type: :child). map(&:id) @@ -62,11 +62,11 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do context 'for top levelnamespaces' do it 'only returns child namespaces with the correct path' do - root_namespace = create(:namespace, path: 'the-path') - _other_path = create(:namespace, path: 'other') - _child_namespace = create(:namespace, + root_namespace = create(:group, path: 'the-path') + _other_path = create(:group, path: 'other') + _child_namespace = create(:group, path: 'the-path', - parent: create(:namespace)) + parent: create(:group)) found_ids = subject.namespaces_for_paths(type: :top_level). map(&:id) @@ -75,11 +75,11 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do end it 'has no namespaces that just look the same' do - root_namespace = create(:namespace, path: 'the-path') - _similar_path = create(:namespace, path: 'not-really-the-path') - _child_namespace = create(:namespace, + root_namespace = create(:group, path: 'the-path') + _similar_path = create(:group, path: 'not-really-the-path') + _child_namespace = create(:group, path: 'the-path', - parent: create(:namespace)) + parent: create(:group)) found_ids = subject.namespaces_for_paths(type: :top_level). map(&:id) @@ -124,10 +124,10 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do describe "#child_ids_for_parent" do it "collects child ids for all levels" do - parent = create(:namespace) - first_child = create(:namespace, parent: parent) - second_child = create(:namespace, parent: parent) - third_child = create(:namespace, parent: second_child) + parent = create(:group) + first_child = create(:group, parent: parent) + second_child = create(:group, parent: parent) + third_child = create(:group, parent: second_child) all_ids = [parent.id, first_child.id, second_child.id, third_child.id] collected_ids = subject.child_ids_for_parent(parent, ids: [parent.id]) @@ -205,9 +205,9 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do end describe '#rename_namespaces' do - let!(:top_level_namespace) { create(:namespace, path: 'the-path') } + let!(:top_level_namespace) { create(:group, path: 'the-path') } let!(:child_namespace) do - create(:namespace, path: 'the-path', parent: create(:namespace)) + create(:group, path: 'the-path', parent: create(:group)) end it 'renames top level namespaces the namespace' do diff --git a/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..df77f4037af5f564a50e1e9b55fa34d4d29cfe4c --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb @@ -0,0 +1,74 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::CartfileLinker, lib: true do + describe '.support?' do + it 'supports Cartfile' do + expect(described_class.support?('Cartfile')).to be_truthy + end + + it 'supports Cartfile.private' do + expect(described_class.support?('Cartfile.private')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('test.Cartfile')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "Cartfile" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + # Require version 2.3.1 or later + github "ReactiveCocoa/ReactiveCocoa" >= 2.3.1 + + # Require version 1.x + github "Mantle/Mantle" ~> 1.0 # (1.0 or later, but less than 2.0) + + # Require exactly version 0.4.1 + github "jspahrsummers/libextobjc" == 0.4.1 + + # Use the latest version + github "jspahrsummers/xcconfigs" + + # Use the branch + github "jspahrsummers/xcconfigs" "branch" + + # Use a project from GitHub Enterprise + github "https://enterprise.local/ghe/desktop/git-error-translations" + + # Use a project from any arbitrary server, on the "development" branch + git "https://enterprise.local/desktop/git-error-translations2.git" "development" + + # Use a local project + git "file:///directory/to/project" "branch" + + # A binary only framework + binary "https://my.domain.com/release/MyFramework.json" ~> 2.3 + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} + end + + it 'links dependencies' do + expect(subject).to include(link('ReactiveCocoa/ReactiveCocoa', 'https://github.com/ReactiveCocoa/ReactiveCocoa')) + expect(subject).to include(link('Mantle/Mantle', 'https://github.com/Mantle/Mantle')) + expect(subject).to include(link('jspahrsummers/libextobjc', 'https://github.com/jspahrsummers/libextobjc')) + expect(subject).to include(link('jspahrsummers/xcconfigs', 'https://github.com/jspahrsummers/xcconfigs')) + end + + it 'links Git repos' do + expect(subject).to include(link('https://enterprise.local/ghe/desktop/git-error-translations', 'https://enterprise.local/ghe/desktop/git-error-translations')) + expect(subject).to include(link('https://enterprise.local/desktop/git-error-translations2.git', 'https://enterprise.local/desktop/git-error-translations2.git')) + end + + it 'links binary-only frameworks' do + expect(subject).to include(link('https://my.domain.com/release/MyFramework.json', 'https://my.domain.com/release/MyFramework.json')) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d7a926e800f567dd4704c7187a3a5f3dd3e265ec --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb @@ -0,0 +1,82 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::ComposerJsonLinker, lib: true do + describe '.support?' do + it 'supports composer.json' do + expect(described_class.support?('composer.json')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('composer.json.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "composer.json" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + { + "name": "laravel/laravel", + "homepage": "https://laravel.com/", + "description": "The Laravel Framework.", + "keywords": ["framework", "laravel"], + "license": "MIT", + "type": "project", + "repositories": [ + { + "type": "git", + "url": "https://github.com/laravel/laravel.git" + } + ], + "require": { + "php": ">=5.5.9", + "laravel/framework": "5.2.*" + }, + "require-dev": { + "fzaninotto/faker": "~1.4", + "mockery/mockery": "0.9.*", + "phpunit/phpunit": "~4.0", + "symfony/css-selector": "2.8.*|3.0.*", + "symfony/dom-crawler": "2.8.*|3.0.*" + } + } + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} + end + + it 'links the module name' do + expect(subject).to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel')) + end + + it 'links the homepage' do + expect(subject).to include(link('https://laravel.com/', 'https://laravel.com/')) + end + + it 'links the repository URL' do + expect(subject).to include(link('https://github.com/laravel/laravel.git', 'https://github.com/laravel/laravel.git')) + end + + it 'links the license' do + expect(subject).to include(link('MIT', 'http://choosealicense.com/licenses/mit/')) + end + + it 'links dependencies' do + expect(subject).to include(link('laravel/framework', 'https://packagist.org/packages/laravel/framework')) + expect(subject).to include(link('fzaninotto/faker', 'https://packagist.org/packages/fzaninotto/faker')) + expect(subject).to include(link('mockery/mockery', 'https://packagist.org/packages/mockery/mockery')) + expect(subject).to include(link('phpunit/phpunit', 'https://packagist.org/packages/phpunit/phpunit')) + expect(subject).to include(link('symfony/css-selector', 'https://packagist.org/packages/symfony/css-selector')) + expect(subject).to include(link('symfony/dom-crawler', 'https://packagist.org/packages/symfony/dom-crawler')) + end + + it 'does not link core dependencies' do + expect(subject).not_to include(link('php', 'https://packagist.org/packages/php')) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb index 2e52097a946dedfa648ccdbc1d3a0387f8ce24e7..3f8335f03ea4464345414e5a8660ece64bc7556f 100644 --- a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::DependencyLinker::GemfileLinker, lib: true do subject { Gitlab::Highlight.highlight(file_name, file_content) } def link(name, url) - %{<a href="#{url}" rel="noopener noreferrer" target="_blank">#{name}</a>} + %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} end it 'links sources' do diff --git a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d4a7140393980a262f6951bd60994679cd003336 --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb @@ -0,0 +1,66 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::GemspecLinker, lib: true do + describe '.support?' do + it 'supports *.gemspec' do + expect(described_class.support?('gitlab_git.gemspec')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('.gemspec.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "gitlab_git.gemspec" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + Gem::Specification.new do |s| + s.name = 'gitlab_git' + s.version = `cat VERSION` + s.date = Time.now.strftime('%Y-%m-%d') + s.summary = "Gitlab::Git library" + s.description = "GitLab wrapper around git objects" + s.authors = ["Dmitriy Zaporozhets"] + s.email = 'dmitriy.zaporozhets@gmail.com' + s.license = 'MIT' + s.files = `git ls-files lib/`.split('\n') << 'VERSION' + s.homepage = 'https://gitlab.com/gitlab-org/gitlab_git' + + s.add_dependency('github-linguist', '~> 4.7.0') + s.add_dependency('activesupport', '~> 4.0') + s.add_dependency('rugged', '~> 0.24.0') + s.add_runtime_dependency('charlock_holmes', '~> 0.7.3') + s.add_development_dependency('listen', '~> 3.0.6') + end + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} + end + + it 'links the gem name' do + expect(subject).to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git')) + end + + it 'links the license' do + expect(subject).to include(link('MIT', 'http://choosealicense.com/licenses/mit/')) + end + + it 'links the homepage' do + expect(subject).to include(link('https://gitlab.com/gitlab-org/gitlab_git', 'https://gitlab.com/gitlab-org/gitlab_git')) + end + + it 'links dependencies' do + expect(subject).to include(link('github-linguist', 'https://rubygems.org/gems/github-linguist')) + expect(subject).to include(link('activesupport', 'https://rubygems.org/gems/activesupport')) + expect(subject).to include(link('rugged', 'https://rubygems.org/gems/rugged')) + expect(subject).to include(link('charlock_holmes', 'https://rubygems.org/gems/charlock_holmes')) + expect(subject).to include(link('listen', 'https://rubygems.org/gems/listen')) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e279e0c9019ea2fe99a7ded3c39f1eb87607d28b --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb @@ -0,0 +1,84 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::GodepsJsonLinker, lib: true do + describe '.support?' do + it 'supports Godeps.json' do + expect(described_class.support?('Godeps.json')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('Godeps.json.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "Godeps.json" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + { + "ImportPath": "gitlab.com/gitlab-org/gitlab-pages", + "GoVersion": "go1.5", + "Packages": [ + "./..." + ], + "Deps": [ + { + "ImportPath": "github.com/kardianos/osext", + "Rev": "efacde03154693404c65e7aa7d461ac9014acd0c" + }, + { + "ImportPath": "github.com/stretchr/testify/assert", + "Rev": "1297dc01ed0a819ff634c89707081a4df43baf6b" + }, + { + "ImportPath": "github.com/stretchr/testify/require", + "Rev": "1297dc01ed0a819ff634c89707081a4df43baf6b" + }, + { + "ImportPath": "gitlab.com/group/project/path", + "Rev": "1297dc01ed0a819ff634c89707081a4df43baf6b" + }, + { + "ImportPath": "gitlab.com/group/subgroup/project.git/path", + "Rev": "1297dc01ed0a819ff634c89707081a4df43baf6b" + }, + { + "ImportPath": "golang.org/x/crypto/ssh/terminal", + "Rev": "1351f936d976c60a0a48d728281922cf63eafb8d" + }, + { + "ImportPath": "golang.org/x/net/http2", + "Rev": "b4e17d61b15679caf2335da776c614169a1b4643" + } + ] + } + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} + end + + it 'links the package name' do + expect(subject).to include(link('gitlab.com/gitlab-org/gitlab-pages', 'https://gitlab.com/gitlab-org/gitlab-pages')) + end + + it 'links GitHub repos' do + expect(subject).to include(link('github.com/kardianos/osext', 'https://github.com/kardianos/osext')) + expect(subject).to include(link('github.com/stretchr/testify/assert', 'https://github.com/stretchr/testify/tree/master/assert')) + expect(subject).to include(link('github.com/stretchr/testify/require', 'https://github.com/stretchr/testify/tree/master/require')) + end + + it 'links GitLab projects' do + expect(subject).to include(link('gitlab.com/group/project/path', 'https://gitlab.com/group/project/tree/master/path')) + expect(subject).to include(link('gitlab.com/group/subgroup/project.git/path', 'https://gitlab.com/group/subgroup/project/tree/master/path')) + end + + it 'links Golang packages' do + expect(subject).to include(link('golang.org/x/net/http2', 'https://godoc.org/golang.org/x/net/http2')) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8c979ae1869d784ee790336336df4df2025604ad --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb @@ -0,0 +1,94 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::PackageJsonLinker, lib: true do + describe '.support?' do + it 'supports package.json' do + expect(described_class.support?('package.json')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('package.json.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "package.json" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + { + "name": "module-name", + "version": "10.3.1", + "repository": { + "type": "git", + "url": "https://github.com/vuejs/vue.git" + }, + "homepage": "https://github.com/vuejs/vue#readme", + "scripts": { + "karma": "karma start config/karma.config.js --single-run" + }, + "dependencies": { + "primus": "*", + "async": "~0.8.0", + "express": "4.2.x", + "bigpipe": "bigpipe/pagelet", + "plates": "https://github.com/flatiron/plates/tarball/master", + "karma": "^1.4.1" + }, + "devDependencies": { + "vows": "^0.7.0", + "assume": "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0", + "pre-commit": "*" + }, + "license": "MIT" + } + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} + end + + it 'links the module name' do + expect(subject).to include(link('module-name', 'https://npmjs.com/package/module-name')) + end + + it 'links the homepage' do + expect(subject).to include(link('https://github.com/vuejs/vue#readme', 'https://github.com/vuejs/vue#readme')) + end + + it 'links the repository URL' do + expect(subject).to include(link('https://github.com/vuejs/vue.git', 'https://github.com/vuejs/vue.git')) + end + + it 'links the license' do + expect(subject).to include(link('MIT', 'http://choosealicense.com/licenses/mit/')) + end + + it 'links dependencies' do + expect(subject).to include(link('primus', 'https://npmjs.com/package/primus')) + expect(subject).to include(link('async', 'https://npmjs.com/package/async')) + expect(subject).to include(link('express', 'https://npmjs.com/package/express')) + expect(subject).to include(link('bigpipe', 'https://npmjs.com/package/bigpipe')) + expect(subject).to include(link('plates', 'https://npmjs.com/package/plates')) + expect(subject).to include(link('karma', 'https://npmjs.com/package/karma')) + expect(subject).to include(link('vows', 'https://npmjs.com/package/vows')) + expect(subject).to include(link('assume', 'https://npmjs.com/package/assume')) + expect(subject).to include(link('pre-commit', 'https://npmjs.com/package/pre-commit')) + end + + it 'links GitHub repos' do + expect(subject).to include(link('bigpipe/pagelet', 'https://github.com/bigpipe/pagelet')) + end + + it 'links Git repos' do + expect(subject).to include(link('https://github.com/flatiron/plates/tarball/master', 'https://github.com/flatiron/plates/tarball/master')) + end + + it 'does not link scripts with the same key as a package' do + expect(subject).not_to include(link('karma start config/karma.config.js --single-run', 'https://github.com/karma start config/karma.config.js --single-run')) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..06007cf97f7d581e78005b51ce9e3dbbe02ea0dd --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::PodfileLinker, lib: true do + describe '.support?' do + it 'supports Podfile' do + expect(described_class.support?('Podfile')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('Podfile.lock')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "Podfile" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + source 'https://github.com/artsy/Specs.git' + source 'https://github.com/CocoaPods/Specs.git' + + platform :ios, '8.0' + use_frameworks! + inhibit_all_warnings! + + target 'Artsy' do + pod 'AFNetworking', "~> 2.5" + pod 'Interstellar/Core', git: 'https://github.com/ashfurrow/Interstellar.git', branch: 'observable-unsubscribe' + end + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} + end + + it 'links sources' do + expect(subject).to include(link('https://github.com/artsy/Specs.git', 'https://github.com/artsy/Specs.git')) + expect(subject).to include(link('https://github.com/CocoaPods/Specs.git', 'https://github.com/CocoaPods/Specs.git')) + end + + it 'links packages' do + expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking')) + expect(subject).to include(link('Interstellar/Core', 'https://cocoapods.org/pods/Interstellar')) + end + + it 'links Git repos' do + expect(subject).to include(link('https://github.com/ashfurrow/Interstellar.git', 'https://github.com/ashfurrow/Interstellar.git')) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d722865264b185692868f7a25e0c6bfc99363e9d --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb @@ -0,0 +1,96 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::PodspecJsonLinker, lib: true do + describe '.support?' do + it 'supports *.podspec.json' do + expect(described_class.support?('Reachability.podspec.json')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('.podspec.json.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "AFNetworking.podspec.json" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + { + "name": "AFNetworking", + "version": "2.0.0", + "license": "MIT", + "summary": "A delightful iOS and OS X networking framework.", + "homepage": "https://github.com/AFNetworking/AFNetworking", + "authors": { + "Mattt Thompson": "m@mattt.me" + }, + "source": { + "git": "https://github.com/AFNetworking/AFNetworking.git", + "tag": "2.0.0", + "submodules": true + }, + "requires_arc": true, + "platforms": { + "ios": "6.0", + "osx": "10.8" + }, + "public_header_files": "AFNetworking/*.h", + "subspecs": [ + { + "name": "NSURLConnection", + "dependencies": { + "AFNetworking/Serialization": [ + + ], + "AFNetworking/Reachability": [ + + ], + "AFNetworking/Security": [ + + ] + }, + "source_files": [ + "AFNetworking/AFURLConnectionOperation.{h,m}", + "AFNetworking/AFHTTPRequestOperation.{h,m}", + "AFNetworking/AFHTTPRequestOperationManager.{h,m}" + ] + } + ] + } + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} + end + + it 'links the gem name' do + expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking')) + end + + it 'links the license' do + expect(subject).to include(link('MIT', 'http://choosealicense.com/licenses/mit/')) + end + + it 'links the homepage' do + expect(subject).to include(link('https://github.com/AFNetworking/AFNetworking', 'https://github.com/AFNetworking/AFNetworking')) + end + + it 'links the source URL' do + expect(subject).to include(link('https://github.com/AFNetworking/AFNetworking.git', 'https://github.com/AFNetworking/AFNetworking.git')) + end + + it 'links dependencies' do + expect(subject).to include(link('AFNetworking/Serialization', 'https://cocoapods.org/pods/AFNetworking')) + expect(subject).to include(link('AFNetworking/Reachability', 'https://cocoapods.org/pods/AFNetworking')) + expect(subject).to include(link('AFNetworking/Security', 'https://cocoapods.org/pods/AFNetworking')) + end + + it 'does not link subspec names' do + expect(subject).not_to include(link('NSURLConnection', 'https://cocoapods.org/pods/NSURLConnection')) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..dfc366b5817e388fedc45c86078ffa22af02306a --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb @@ -0,0 +1,69 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::PodspecLinker, lib: true do + describe '.support?' do + it 'supports *.podspec' do + expect(described_class.support?('Reachability.podspec')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('.podspec.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "Reachability.podspec" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + Pod::Spec.new do |spec| + spec.name = 'Reachability' + spec.version = '3.1.0' + spec.license = { :type => 'GPL-3.0' } + spec.license = "MIT" + spec.license = { type: 'Apache-2.0' } + spec.homepage = 'https://github.com/tonymillion/Reachability' + spec.authors = { 'Tony Million' => 'tonymillion@gmail.com' } + spec.summary = 'ARC and GCD Compatible Reachability Class for iOS and OS X.' + spec.source = { :git => 'https://github.com/tonymillion/Reachability.git', :tag => 'v3.1.0' } + spec.source_files = 'Reachability.{h,m}' + spec.framework = 'SystemConfiguration' + + spec.dependency 'AFNetworking', '~> 1.0' + spec.dependency 'RestKit/CoreData', '~> 0.20.0' + spec.ios.dependency 'MBProgressHUD', '~> 0.5' + end + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} + end + + it 'links the gem name' do + expect(subject).to include(link('Reachability', 'https://cocoapods.org/pods/Reachability')) + end + + it 'links the license' do + expect(subject).to include(link('GPL-3.0', 'http://choosealicense.com/licenses/gpl-3.0/')) + expect(subject).to include(link('MIT', 'http://choosealicense.com/licenses/mit/')) + expect(subject).to include(link('Apache-2.0', 'http://choosealicense.com/licenses/apache-2.0/')) + end + + it 'links the homepage' do + expect(subject).to include(link('https://github.com/tonymillion/Reachability', 'https://github.com/tonymillion/Reachability')) + end + + it 'links the source URL' do + expect(subject).to include(link('https://github.com/tonymillion/Reachability.git', 'https://github.com/tonymillion/Reachability.git')) + end + + it 'links dependencies' do + expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking')) + expect(subject).to include(link('RestKit/CoreData', 'https://cocoapods.org/pods/RestKit')) + expect(subject).to include(link('MBProgressHUD', 'https://cocoapods.org/pods/MBProgressHUD')) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4da8821726cd2a16a172a5a2a976e6f03c2a8e24 --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb @@ -0,0 +1,87 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::RequirementsTxtLinker, lib: true do + describe '.support?' do + it 'supports requirements.txt' do + expect(described_class.support?('requirements.txt')).to be_truthy + end + + it 'supports doc-requirements.txt' do + expect(described_class.support?('doc-requirements.txt')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('requirements')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "requirements.txt" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + # + ####### example-requirements.txt ####### + # + ###### Requirements without Version Specifiers ###### + nose + nose-cov + beautifulsoup4 + # + ###### Requirements with Version Specifiers ###### + # See https://www.python.org/dev/peps/pep-0440/#version-specifiers + docopt == 0.6.1 # Version Matching. Must be version 0.6.1 + keyring >= 4.1.1 # Minimum version 4.1.1 + coverage != 3.5 # Version Exclusion. Anything except version 3.5 + Mopidy-Dirble ~= 1.1 # Compatible release. Same as >= 1.1, == 1.* + # + ###### Refer to other requirements files ###### + -r other-requirements.txt + # + # + ###### A particular file ###### + ./downloads/numpy-1.9.2-cp34-none-win32.whl + http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl + # + ###### Additional Requirements without Version Specifiers ###### + # Same as 1st section, just here to show that you can put things in any order. + rejected + green + # + + Jinja2>=2.3 + Pygments>=1.2 + Sphinx>=1.3 + docutils>=0.7 + markupsafe + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} + end + + it 'links dependencies' do + expect(subject).to include(link('nose', 'https://pypi.python.org/pypi/nose')) + expect(subject).to include(link('nose-cov', 'https://pypi.python.org/pypi/nose-cov')) + expect(subject).to include(link('beautifulsoup4', 'https://pypi.python.org/pypi/beautifulsoup4')) + expect(subject).to include(link('docopt', 'https://pypi.python.org/pypi/docopt')) + expect(subject).to include(link('keyring', 'https://pypi.python.org/pypi/keyring')) + expect(subject).to include(link('coverage', 'https://pypi.python.org/pypi/coverage')) + expect(subject).to include(link('Mopidy-Dirble', 'https://pypi.python.org/pypi/Mopidy-Dirble')) + expect(subject).to include(link('rejected', 'https://pypi.python.org/pypi/rejected')) + expect(subject).to include(link('green', 'https://pypi.python.org/pypi/green')) + expect(subject).to include(link('Jinja2', 'https://pypi.python.org/pypi/Jinja2')) + expect(subject).to include(link('Pygments', 'https://pypi.python.org/pypi/Pygments')) + expect(subject).to include(link('Sphinx', 'https://pypi.python.org/pypi/Sphinx')) + expect(subject).to include(link('docutils', 'https://pypi.python.org/pypi/docutils')) + expect(subject).to include(link('markupsafe', 'https://pypi.python.org/pypi/markupsafe')) + end + + it 'links URLs' do + expect(subject).to include(link('http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl', 'http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl')) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb index 03d5b61d70cd8f60d7df4956a3a7377403c94a82..3d1cfbcfbf710082817d5f28750b420ec6aad08e 100644 --- a/spec/lib/gitlab/dependency_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker_spec.rb @@ -9,5 +9,77 @@ describe Gitlab::DependencyLinker, lib: true do described_class.link(blob_name, nil, nil) end + + it 'links using GemspecLinker' do + blob_name = 'gitlab_git.gemspec' + + expect(described_class::GemspecLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using PackageJsonLinker' do + blob_name = 'package.json' + + expect(described_class::PackageJsonLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using ComposerJsonLinker' do + blob_name = 'composer.json' + + expect(described_class::ComposerJsonLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using PodfileLinker' do + blob_name = 'Podfile' + + expect(described_class::PodfileLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using PodspecLinker' do + blob_name = 'Reachability.podspec' + + expect(described_class::PodspecLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using PodspecJsonLinker' do + blob_name = 'AFNetworking.podspec.json' + + expect(described_class::PodspecJsonLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using CartfileLinker' do + blob_name = 'Cartfile' + + expect(described_class::CartfileLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using GodepsJsonLinker' do + blob_name = 'Godeps.json' + + expect(described_class::GodepsJsonLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using RequirementsTxtLinker' do + blob_name = 'requirements.txt' + + expect(described_class::RequirementsTxtLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end end end diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index cdf0af6d7ef7521c84644721d3d2b8732a5b899b..7095104d75cc8ea74cd0e2a68fe00783195ca819 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -22,7 +22,7 @@ describe Gitlab::Diff::Position, lib: true do it "returns the correct diff file" do diff_file = subject.diff_file(project.repository) - expect(diff_file.new_file).to be true + expect(diff_file.new_file?).to be true expect(diff_file.new_path).to eq(subject.new_path) expect(diff_file.diff_refs).to eq(subject.diff_refs) end @@ -314,7 +314,7 @@ describe Gitlab::Diff::Position, lib: true do it "returns the correct diff file" do diff_file = subject.diff_file(project.repository) - expect(diff_file.deleted_file).to be true + expect(diff_file.deleted_file?).to be true expect(diff_file.old_path).to eq(subject.old_path) expect(diff_file.diff_refs).to eq(subject.diff_refs) end @@ -356,7 +356,7 @@ describe Gitlab::Diff::Position, lib: true do it "returns the correct diff file" do diff_file = subject.diff_file(project.repository) - expect(diff_file.new_file).to be true + expect(diff_file.new_file?).to be true expect(diff_file.new_path).to eq(subject.new_path) expect(diff_file.diff_refs).to eq(subject.diff_refs) end diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index 122c93dcd690bb146063eb1621abe478bbaae6c5..ae617b313c59b3f53043ebbeb3dfad668480e026 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -10,7 +10,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do no_collapse: no_collapse ) end - let(:iterator) { Array.new(file_count, fake_diff(line_length, line_count)) } + let(:iterator) { MutatingConstantIterator.new(file_count, fake_diff(line_length, line_count)) } let(:file_count) { 0 } let(:line_length) { 1 } let(:line_count) { 1 } @@ -64,7 +64,15 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do subject { super().real_size } it { is_expected.to eq('3') } end - it { expect(subject.size).to eq(3) } + + describe '#size' do + it { expect(subject.size).to eq(3) } + + it 'does not change after peeking' do + subject.any? + expect(subject.size).to eq(3) + end + end context 'when limiting is disabled' do let(:all_diffs) { true } @@ -83,7 +91,15 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do subject { super().real_size } it { is_expected.to eq('3') } end - it { expect(subject.size).to eq(3) } + + describe '#size' do + it { expect(subject.size).to eq(3) } + + it 'does not change after peeking' do + subject.any? + expect(subject.size).to eq(3) + end + end end end @@ -457,4 +473,22 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do def fake_diff(line_length, line_count) { 'diff' => "#{'a' * line_length}\n" * line_count } end + + class MutatingConstantIterator + include Enumerable + + def initialize(count, value) + @count = count + @value = value + end + + def each + loop do + break if @count.zero? + # It is critical to decrement before yielding. We may never reach the lines after 'yield'. + @count -= 1 + yield @value + end + end + end end diff --git a/spec/lib/gitlab/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5d0ed1522b39e391dfbfa544b6be4655dd7c1798 --- /dev/null +++ b/spec/lib/gitlab/group_hierarchy_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Gitlab::GroupHierarchy, :postgresql do + let!(:parent) { create(:group) } + let!(:child1) { create(:group, parent: parent) } + let!(:child2) { create(:group, parent: child1) } + + describe '#base_and_ancestors' do + let(:relation) do + described_class.new(Group.where(id: child2.id)).base_and_ancestors + end + + it 'includes the base rows' do + expect(relation).to include(child2) + end + + it 'includes all of the ancestors' do + expect(relation).to include(parent, child1) + end + end + + describe '#base_and_descendants' do + let(:relation) do + described_class.new(Group.where(id: parent.id)).base_and_descendants + end + + it 'includes the base rows' do + expect(relation).to include(parent) + end + + it 'includes all the descendants' do + expect(relation).to include(child1, child2) + end + end + + describe '#all_groups' do + let(:relation) do + described_class.new(Group.where(id: child1.id)).all_groups + end + + it 'includes the base rows' do + expect(relation).to include(child1) + end + + it 'includes the ancestors' do + expect(relation).to include(parent) + end + + it 'includes the descendants' do + expect(relation).to include(child2) + end + end +end diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb index 45ccd3d6459d6234c5389378642cb3535e8cb733..61c10d47434bf7c6a74b51279bf136474f23e6a7 100644 --- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb @@ -1,6 +1,24 @@ require 'spec_helper' describe Gitlab::HealthChecks::FsShardsCheck do + def command_exists?(command) + _, status = Gitlab::Popen.popen(%W{ #{command} 1 echo }) + status == 0 + rescue Errno::ENOENT + false + end + + def timeout_command + @timeout_command ||= + if command_exists?('timeout') + 'timeout' + elsif command_exists?('gtimeout') + 'gtimeout' + else + '' + end + end + let(:metric_class) { Gitlab::HealthChecks::Metric } let(:result_class) { Gitlab::HealthChecks::Result } let(:repository_storages) { [:default] } @@ -15,6 +33,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do before do allow(described_class).to receive(:repository_storages) { repository_storages } allow(described_class).to receive(:storages_paths) { storages_paths } + stub_const('Gitlab::HealthChecks::FsShardsCheck::TIMEOUT_EXECUTABLE', timeout_command) end after do @@ -78,40 +97,76 @@ describe Gitlab::HealthChecks::FsShardsCheck do }.with_indifferent_access end - it { is_expected.to include(metric_class.new(:filesystem_accessible, 0, shard: :default)) } - it { is_expected.to include(metric_class.new(:filesystem_readable, 0, shard: :default)) } - it { is_expected.to include(metric_class.new(:filesystem_writable, 0, shard: :default)) } + it { is_expected.to all(have_attributes(labels: { shard: :default })) } + + it { is_expected.to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) } + it { is_expected.to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) } + it { is_expected.to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) } - it { is_expected.to include(have_attributes(name: :filesystem_access_latency, value: be >= 0, labels: { shard: :default })) } - it { is_expected.to include(have_attributes(name: :filesystem_read_latency, value: be >= 0, labels: { shard: :default })) } - it { is_expected.to include(have_attributes(name: :filesystem_write_latency, value: be >= 0, labels: { shard: :default })) } + it { is_expected.to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) } + it { is_expected.to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) } + it { is_expected.to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) } end context 'storage points to directory that has both read and write rights' do before do FileUtils.chmod_R(0755, tmp_dir) end + it { is_expected.to all(have_attributes(labels: { shard: :default })) } - it { is_expected.to include(metric_class.new(:filesystem_accessible, 1, shard: :default)) } - it { is_expected.to include(metric_class.new(:filesystem_readable, 1, shard: :default)) } - it { is_expected.to include(metric_class.new(:filesystem_writable, 1, shard: :default)) } + it { is_expected.to include(an_object_having_attributes(name: :filesystem_accessible, value: 1)) } + it { is_expected.to include(an_object_having_attributes(name: :filesystem_readable, value: 1)) } + it { is_expected.to include(an_object_having_attributes(name: :filesystem_writable, value: 1)) } - it { is_expected.to include(have_attributes(name: :filesystem_access_latency, value: be >= 0, labels: { shard: :default })) } - it { is_expected.to include(have_attributes(name: :filesystem_read_latency, value: be >= 0, labels: { shard: :default })) } - it { is_expected.to include(have_attributes(name: :filesystem_write_latency, value: be >= 0, labels: { shard: :default })) } + it { is_expected.to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) } + it { is_expected.to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) } + it { is_expected.to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) } + end + end + end + + context 'when timeout kills fs checks' do + before do + stub_const('Gitlab::HealthChecks::FsShardsCheck::COMMAND_TIMEOUT', '1') + + allow(described_class).to receive(:exec_with_timeout).and_wrap_original { |m| m.call(%w(sleep 60)) } + FileUtils.chmod_R(0755, tmp_dir) + end + + describe '#readiness' do + subject { described_class.readiness } + + it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: :default)) } + end + + describe '#metrics' do + subject { described_class.metrics } + + it 'provides metrics' do + expect(subject).to all(have_attributes(labels: { shard: :default })) + + expect(subject).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) + + expect(subject).to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) end end end context 'when popen always finds required binaries' do before do - allow(Gitlab::Popen).to receive(:popen).and_wrap_original do |method, *args, &block| + allow(described_class).to receive(:exec_with_timeout).and_wrap_original do |method, *args, &block| begin method.call(*args, &block) - rescue RuntimeError + rescue RuntimeError, Errno::ENOENT raise 'expected not to happen' end end + + stub_const('Gitlab::HealthChecks::FsShardsCheck::COMMAND_TIMEOUT', '10') end it_behaves_like 'filesystem checks' diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index e57b3053871b211826e046463801d816e505fa93..a20cef3b00025f1033c5b7f38f2ea669bf74faa0 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -59,8 +59,6 @@ describe Gitlab::Highlight, lib: true do end describe '#highlight' do - subject { described_class.highlight(file_name, file_content, nowrap: false) } - it 'links dependencies via DependencyLinker' do expect(Gitlab::DependencyLinker).to receive(:link). with('file.name', 'Contents', anything).and_call_original diff --git a/spec/lib/gitlab/i18n_spec.rb b/spec/lib/gitlab/i18n_spec.rb index 52f2614d5cab6d08b5a2194142d7656a4c4be491..a3dbeaa3753ebd20e94214bf225f34ae5f97c6c4 100644 --- a/spec/lib/gitlab/i18n_spec.rb +++ b/spec/lib/gitlab/i18n_spec.rb @@ -1,27 +1,27 @@ require 'spec_helper' -module Gitlab - describe I18n, lib: true do - let(:user) { create(:user, preferred_language: 'es') } +describe Gitlab::I18n, lib: true do + let(:user) { create(:user, preferred_language: 'es') } - describe '.set_locale' do - it 'sets the locale based on current user preferred language' do - Gitlab::I18n.set_locale(user) + describe '.locale=' do + after { described_class.use_default_locale } - expect(FastGettext.locale).to eq('es') - expect(::I18n.locale).to eq(:es) - end + it 'sets the locale based on current user preferred language' do + described_class.locale = user.preferred_language + + expect(FastGettext.locale).to eq('es') + expect(::I18n.locale).to eq(:es) end + end - describe '.reset_locale' do - it 'resets the locale to the default language' do - Gitlab::I18n.set_locale(user) + describe '.use_default_locale' do + it 'resets the locale to the default language' do + described_class.locale = user.preferred_language - Gitlab::I18n.reset_locale + described_class.use_default_locale - expect(FastGettext.locale).to eq('en') - expect(::I18n.locale).to eq(:en) - end + expect(FastGettext.locale).to eq('en') + expect(::I18n.locale).to eq(:en) end end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 34f617e23a58a3d3a8f785aa04d77c1b538acc1c..2e9646286dfd599d73c913aad59c2fb0399de25c 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -131,6 +131,7 @@ services: - service_hook hooks: - project +- web_hook_logs protected_branches: - project - merge_access_levels diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index b9d4e59e770353715ae1cb5dd869e8d15358f304..3e0291c9ae9c288c86a8531661b14606e00bba1b 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe Gitlab::ImportExport::MembersMapper, services: true do describe 'map members' do - let(:user) { create(:admin, authorized_projects_populated: true) } + let(:user) { create(:admin) } let(:project) { create(:empty_project, :public, name: 'searchable_project') } - let(:user2) { create(:user, authorized_projects_populated: true) } + let(:user2) { create(:user) } let(:exported_user_id) { 99 } let(:exported_members) do [{ @@ -74,7 +74,7 @@ describe Gitlab::ImportExport::MembersMapper, services: true do end context 'user is not an admin' do - let(:user) { create(:user, authorized_projects_populated: true) } + let(:user) { create(:user) } it 'does not map a project member' do expect(members_mapper.map[exported_user_id]).to eq(user.id) @@ -94,7 +94,7 @@ describe Gitlab::ImportExport::MembersMapper, services: true do end context 'importer same as group member' do - let(:user2) { create(:admin, authorized_projects_populated: true) } + let(:user2) { create(:admin) } let(:group) { create(:group) } let(:project) { create(:empty_project, :public, name: 'searchable_project', namespace: group) } let(:members_mapper) do diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1eea710c80ba1f965c72040c73e5c0ca7347200d --- /dev/null +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -0,0 +1,384 @@ +# coding: utf-8 +require 'spec_helper' + +describe Gitlab::PathRegex, lib: true do + # Pass in a full path to remove the format segment: + # `/ci/lint(.:format)` -> `/ci/lint` + def without_format(path) + path.split('(', 2)[0] + end + + # Pass in a full path and get the last segment before a wildcard + # That's not a parameter + # `/*namespace_id/:project_id/builds/artifacts/*ref_name_and_path` + # -> 'builds/artifacts' + def path_before_wildcard(path) + path = path.gsub(STARTING_WITH_NAMESPACE, "") + path_segments = path.split('/').reject(&:empty?) + wildcard_index = path_segments.index { |segment| parameter?(segment) } + + segments_before_wildcard = path_segments[0..wildcard_index - 1] + + segments_before_wildcard.join('/') + end + + def parameter?(segment) + segment =~ /[*:]/ + end + + # If the path is reserved. Then no conflicting paths can# be created for any + # route using this reserved word. + # + # Both `builds/artifacts` & `build` are covered by reserving the word + # `build` + def wildcards_include?(path) + described_class::PROJECT_WILDCARD_ROUTES.include?(path) || + described_class::PROJECT_WILDCARD_ROUTES.include?(path.split('/').first) + end + + def failure_message(missing_words, constant_name, migration_helper) + missing_words = Array(missing_words) + <<-MSG + Found new routes that could cause conflicts with existing namespaced routes + for groups or projects. + + Add <#{missing_words.join(', ')}> to `Gitlab::PathRegex::#{constant_name} + to make sure no projects or namespaces can be created with those paths. + + To rename any existing records with those paths you can use the + `Gitlab::Database::RenameReservedpathsMigration::<VERSION>.#{migration_helper}` + migration helper. + + Make sure to make a note of the renamed records in the release blog post. + + MSG + end + + let(:all_routes) do + route_set = Rails.application.routes + routes_collection = route_set.routes + routes_array = routes_collection.routes + routes_array.map { |route| route.path.spec.to_s } + end + + let(:routes_without_format) { all_routes.map { |path| without_format(path) } } + + # Routes not starting with `/:` or `/*` + # all routes not starting with a param + let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } } + + let(:top_level_words) do + routes_not_starting_in_wildcard.map do |route| + route.split('/')[1] + end.compact.uniq + end + + # All routes that start with a namespaced path, that have 1 or more + # path-segments before having another wildcard parameter. + # - Starting with paths: + # - `/*namespace_id/:project_id/` + # - `/*namespace_id/:id/` + # - Followed by one or more path-parts not starting with `:` or `*` + # - Followed by a path-part that includes a wildcard parameter `*` + # At the time of writing these routes match: http://rubular.com/r/Rv2pDE5Dvw + STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id} + NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*} + ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*} + WILDCARD_SEGMENT = %r{\*} + let(:namespaced_wildcard_routes) do + routes_without_format.select do |p| + p =~ %r{#{STARTING_WITH_NAMESPACE}/#{NON_PARAM_PARTS}/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}} + end + end + + # This will return all paths that are used in a namespaced route + # before another wildcard path: + # + # /*namespace_id/:project_id/builds/artifacts/*ref_name_and_path + # /*namespace_id/:project_id/info/lfs/objects/*oid + # /*namespace_id/:project_id/commits/*id + # /*namespace_id/:project_id/builds/:build_id/artifacts/file/*path + # -> ['builds/artifacts', 'info/lfs/objects', 'commits', 'artifacts/file'] + let(:all_wildcard_paths) do + namespaced_wildcard_routes.map do |route| + path_before_wildcard(route) + end.uniq + end + + STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/} + let(:group_routes) do + routes_without_format.select do |path| + path =~ STARTING_WITH_GROUP + end + end + + let(:paths_after_group_id) do + group_routes.map do |route| + route.gsub(STARTING_WITH_GROUP, '').split('/').first + end.uniq + end + + describe 'TOP_LEVEL_ROUTES' do + it 'includes all the top level namespaces' do + failure_block = lambda do + missing_words = top_level_words - described_class::TOP_LEVEL_ROUTES + failure_message(missing_words, 'TOP_LEVEL_ROUTES', 'rename_root_paths') + end + + expect(described_class::TOP_LEVEL_ROUTES) + .to include(*top_level_words), failure_block + end + end + + describe 'GROUP_ROUTES' do + it "don't contain a second wildcard" do + failure_block = lambda do + missing_words = paths_after_group_id - described_class::GROUP_ROUTES + failure_message(missing_words, 'GROUP_ROUTES', 'rename_child_paths') + end + + expect(described_class::GROUP_ROUTES) + .to include(*paths_after_group_id), failure_block + end + end + + describe 'PROJECT_WILDCARD_ROUTES' do + it 'includes all paths that can be used after a namespace/project path' do + aggregate_failures do + all_wildcard_paths.each do |path| + expect(wildcards_include?(path)) + .to be(true), failure_message(path, 'PROJECT_WILDCARD_ROUTES', 'rename_wildcard_paths') + end + end + end + end + + describe '.root_namespace_path_regex' do + subject { described_class.root_namespace_path_regex } + + it 'rejects top level routes' do + expect(subject).not_to match('admin/') + expect(subject).not_to match('api/') + expect(subject).not_to match('.well-known/') + end + + it 'accepts project wildcard routes' do + expect(subject).to match('blob/') + expect(subject).to match('edit/') + expect(subject).to match('wikis/') + end + + it 'accepts group routes' do + expect(subject).to match('activity/') + expect(subject).to match('group_members/') + expect(subject).to match('subgroups/') + end + + it 'is not case sensitive' do + expect(subject).not_to match('Users/') + end + + it 'does not allow extra slashes' do + expect(subject).not_to match('/blob/') + expect(subject).not_to match('blob//') + end + end + + describe '.full_namespace_path_regex' do + subject { described_class.full_namespace_path_regex } + + context 'at the top level' do + context 'when the final level' do + it 'rejects top level routes' do + expect(subject).not_to match('admin/') + expect(subject).not_to match('api/') + expect(subject).not_to match('.well-known/') + end + + it 'accepts project wildcard routes' do + expect(subject).to match('blob/') + expect(subject).to match('edit/') + expect(subject).to match('wikis/') + end + + it 'accepts group routes' do + expect(subject).to match('activity/') + expect(subject).to match('group_members/') + expect(subject).to match('subgroups/') + end + end + + context 'when more levels follow' do + it 'rejects top level routes' do + expect(subject).not_to match('admin/more/') + expect(subject).not_to match('api/more/') + expect(subject).not_to match('.well-known/more/') + end + + it 'accepts project wildcard routes' do + expect(subject).to match('blob/more/') + expect(subject).to match('edit/more/') + expect(subject).to match('wikis/more/') + expect(subject).to match('environments/folders/') + expect(subject).to match('info/lfs/objects/') + end + + it 'accepts group routes' do + expect(subject).to match('activity/more/') + expect(subject).to match('group_members/more/') + expect(subject).to match('subgroups/more/') + end + end + end + + context 'at the second level' do + context 'when the final level' do + it 'accepts top level routes' do + expect(subject).to match('root/admin/') + expect(subject).to match('root/api/') + expect(subject).to match('root/.well-known/') + end + + it 'rejects project wildcard routes' do + expect(subject).not_to match('root/blob/') + expect(subject).not_to match('root/edit/') + expect(subject).not_to match('root/wikis/') + expect(subject).not_to match('root/environments/folders/') + expect(subject).not_to match('root/info/lfs/objects/') + end + + it 'rejects group routes' do + expect(subject).not_to match('root/activity/') + expect(subject).not_to match('root/group_members/') + expect(subject).not_to match('root/subgroups/') + end + end + + context 'when more levels follow' do + it 'accepts top level routes' do + expect(subject).to match('root/admin/more/') + expect(subject).to match('root/api/more/') + expect(subject).to match('root/.well-known/more/') + end + + it 'rejects project wildcard routes' do + expect(subject).not_to match('root/blob/more/') + expect(subject).not_to match('root/edit/more/') + expect(subject).not_to match('root/wikis/more/') + expect(subject).not_to match('root/environments/folders/more/') + expect(subject).not_to match('root/info/lfs/objects/more/') + end + + it 'rejects group routes' do + expect(subject).not_to match('root/activity/more/') + expect(subject).not_to match('root/group_members/more/') + expect(subject).not_to match('root/subgroups/more/') + end + end + end + + it 'is not case sensitive' do + expect(subject).not_to match('root/Blob/') + end + + it 'does not allow extra slashes' do + expect(subject).not_to match('/root/admin/') + expect(subject).not_to match('root/admin//') + end + end + + describe '.project_path_regex' do + subject { described_class.project_path_regex } + + it 'accepts top level routes' do + expect(subject).to match('admin/') + expect(subject).to match('api/') + expect(subject).to match('.well-known/') + end + + it 'rejects project wildcard routes' do + expect(subject).not_to match('blob/') + expect(subject).not_to match('edit/') + expect(subject).not_to match('wikis/') + expect(subject).not_to match('environments/folders/') + expect(subject).not_to match('info/lfs/objects/') + end + + it 'accepts group routes' do + expect(subject).to match('activity/') + expect(subject).to match('group_members/') + expect(subject).to match('subgroups/') + end + + it 'is not case sensitive' do + expect(subject).not_to match('Blob/') + end + + it 'does not allow extra slashes' do + expect(subject).not_to match('/admin/') + expect(subject).not_to match('admin//') + end + end + + describe '.full_project_path_regex' do + subject { described_class.full_project_path_regex } + + it 'accepts top level routes' do + expect(subject).to match('root/admin/') + expect(subject).to match('root/api/') + expect(subject).to match('root/.well-known/') + end + + it 'rejects project wildcard routes' do + expect(subject).not_to match('root/blob/') + expect(subject).not_to match('root/edit/') + expect(subject).not_to match('root/wikis/') + expect(subject).not_to match('root/environments/folders/') + expect(subject).not_to match('root/info/lfs/objects/') + end + + it 'accepts group routes' do + expect(subject).to match('root/activity/') + expect(subject).to match('root/group_members/') + expect(subject).to match('root/subgroups/') + end + + it 'is not case sensitive' do + expect(subject).not_to match('root/Blob/') + end + + it 'does not allow extra slashes' do + expect(subject).not_to match('/root/admin/') + expect(subject).not_to match('root/admin//') + end + end + + describe '.namespace_format_regex' do + subject { described_class.namespace_format_regex } + + it { is_expected.to match('gitlab-ce') } + it { is_expected.to match('gitlab_git') } + it { is_expected.to match('_underscore.js') } + it { is_expected.to match('100px.com') } + it { is_expected.to match('gitlab.org') } + it { is_expected.not_to match('?gitlab') } + it { is_expected.not_to match('git lab') } + it { is_expected.not_to match('gitlab.git') } + it { is_expected.not_to match('gitlab.org.') } + it { is_expected.not_to match('gitlab.org/') } + it { is_expected.not_to match('/gitlab.org') } + it { is_expected.not_to match('gitlab git') } + end + + describe '.project_path_format_regex' do + subject { described_class.project_path_format_regex } + + it { is_expected.to match('gitlab-ce') } + it { is_expected.to match('gitlab_git') } + it { is_expected.to match('_underscore.js') } + it { is_expected.to match('100px.com') } + it { is_expected.not_to match('?gitlab') } + it { is_expected.not_to match('git lab') } + it { is_expected.not_to match('gitlab.git') } + end +end diff --git a/spec/lib/gitlab/project_authorizations_spec.rb b/spec/lib/gitlab/project_authorizations_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..67321f43710bef3b1a094a5f61c4ccabca516cfc --- /dev/null +++ b/spec/lib/gitlab/project_authorizations_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe Gitlab::ProjectAuthorizations do + let(:group) { create(:group) } + let!(:owned_project) { create(:empty_project) } + let!(:other_project) { create(:empty_project) } + let!(:group_project) { create(:empty_project, namespace: group) } + + let(:user) { owned_project.namespace.owner } + + def map_access_levels(rows) + rows.each_with_object({}) do |row, hash| + hash[row.project_id] = row.access_level + end + end + + before do + other_project.team << [user, :reporter] + group.add_developer(user) + end + + let(:authorizations) do + klass = if Group.supports_nested_groups? + Gitlab::ProjectAuthorizations::WithNestedGroups + else + Gitlab::ProjectAuthorizations::WithoutNestedGroups + end + + klass.new(user).calculate + end + + it 'returns the correct number of authorizations' do + expect(authorizations.length).to eq(3) + end + + it 'includes the correct projects' do + expect(authorizations.pluck(:project_id)). + to include(owned_project.id, other_project.id, group_project.id) + end + + it 'includes the correct access levels' do + mapping = map_access_levels(authorizations) + + expect(mapping[owned_project.id]).to eq(Gitlab::Access::MASTER) + expect(mapping[other_project.id]).to eq(Gitlab::Access::REPORTER) + expect(mapping[group_project.id]).to eq(Gitlab::Access::DEVELOPER) + end + + if Group.supports_nested_groups? + context 'with nested groups' do + let!(:nested_group) { create(:group, parent: group) } + let!(:nested_project) { create(:empty_project, namespace: nested_group) } + + it 'includes nested groups' do + expect(authorizations.pluck(:project_id)).to include(nested_project.id) + end + + it 'inherits access levels when the user is not a member of a nested group' do + mapping = map_access_levels(authorizations) + + expect(mapping[nested_project.id]).to eq(Gitlab::Access::DEVELOPER) + end + + it 'uses the greatest access level when a user is a member of a nested group' do + nested_group.add_master(user) + + mapping = map_access_levels(authorizations) + + expect(mapping[nested_project.id]).to eq(Gitlab::Access::MASTER) + end + end + end +end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 1b8690ba613820f8b727a7ae551ac5103fa975ed..3d22784909d3a07285a27f6ba802ae977df9e4b8 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -123,8 +123,8 @@ describe Gitlab::ProjectSearchResults, lib: true do context 'when wiki is internal' do let(:project) { create(:project, :public, :wiki_private) } - it 'finds wiki blobs for members' do - project.add_reporter(user) + it 'finds wiki blobs for guest' do + project.add_guest(user) is_expected.not_to be_empty end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index a7d1283acb8775b9df0899dae97a8209ed5c218a..0bee892fe0c3afc5fb5e718d506bc8108294566c 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -2,386 +2,6 @@ require 'spec_helper' describe Gitlab::Regex, lib: true do - # Pass in a full path to remove the format segment: - # `/ci/lint(.:format)` -> `/ci/lint` - def without_format(path) - path.split('(', 2)[0] - end - - # Pass in a full path and get the last segment before a wildcard - # That's not a parameter - # `/*namespace_id/:project_id/builds/artifacts/*ref_name_and_path` - # -> 'builds/artifacts' - def path_before_wildcard(path) - path = path.gsub(STARTING_WITH_NAMESPACE, "") - path_segments = path.split('/').reject(&:empty?) - wildcard_index = path_segments.index { |segment| parameter?(segment) } - - segments_before_wildcard = path_segments[0..wildcard_index - 1] - - segments_before_wildcard.join('/') - end - - def parameter?(segment) - segment =~ /[*:]/ - end - - # If the path is reserved. Then no conflicting paths can# be created for any - # route using this reserved word. - # - # Both `builds/artifacts` & `build` are covered by reserving the word - # `build` - def wildcards_include?(path) - described_class::PROJECT_WILDCARD_ROUTES.include?(path) || - described_class::PROJECT_WILDCARD_ROUTES.include?(path.split('/').first) - end - - def failure_message(missing_words, constant_name, migration_helper) - missing_words = Array(missing_words) - <<-MSG - Found new routes that could cause conflicts with existing namespaced routes - for groups or projects. - - Add <#{missing_words.join(', ')}> to `Gitlab::Regex::#{constant_name} - to make sure no projects or namespaces can be created with those paths. - - To rename any existing records with those paths you can use the - `Gitlab::Database::RenameReservedpathsMigration::<VERSION>.#{migration_helper}` - migration helper. - - Make sure to make a note of the renamed records in the release blog post. - - MSG - end - - let(:all_routes) do - route_set = Rails.application.routes - routes_collection = route_set.routes - routes_array = routes_collection.routes - routes_array.map { |route| route.path.spec.to_s } - end - - let(:routes_without_format) { all_routes.map { |path| without_format(path) } } - - # Routes not starting with `/:` or `/*` - # all routes not starting with a param - let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } } - - let(:top_level_words) do - routes_not_starting_in_wildcard.map do |route| - route.split('/')[1] - end.compact.uniq - end - - # All routes that start with a namespaced path, that have 1 or more - # path-segments before having another wildcard parameter. - # - Starting with paths: - # - `/*namespace_id/:project_id/` - # - `/*namespace_id/:id/` - # - Followed by one or more path-parts not starting with `:` or `*` - # - Followed by a path-part that includes a wildcard parameter `*` - # At the time of writing these routes match: http://rubular.com/r/Rv2pDE5Dvw - STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id} - NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*} - ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*} - WILDCARD_SEGMENT = %r{\*} - let(:namespaced_wildcard_routes) do - routes_without_format.select do |p| - p =~ %r{#{STARTING_WITH_NAMESPACE}/#{NON_PARAM_PARTS}/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}} - end - end - - # This will return all paths that are used in a namespaced route - # before another wildcard path: - # - # /*namespace_id/:project_id/builds/artifacts/*ref_name_and_path - # /*namespace_id/:project_id/info/lfs/objects/*oid - # /*namespace_id/:project_id/commits/*id - # /*namespace_id/:project_id/builds/:build_id/artifacts/file/*path - # -> ['builds/artifacts', 'info/lfs/objects', 'commits', 'artifacts/file'] - let(:all_wildcard_paths) do - namespaced_wildcard_routes.map do |route| - path_before_wildcard(route) - end.uniq - end - - STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/} - let(:group_routes) do - routes_without_format.select do |path| - path =~ STARTING_WITH_GROUP - end - end - - let(:paths_after_group_id) do - group_routes.map do |route| - route.gsub(STARTING_WITH_GROUP, '').split('/').first - end.uniq - end - - describe 'TOP_LEVEL_ROUTES' do - it 'includes all the top level namespaces' do - failure_block = lambda do - missing_words = top_level_words - described_class::TOP_LEVEL_ROUTES - failure_message(missing_words, 'TOP_LEVEL_ROUTES', 'rename_root_paths') - end - - expect(described_class::TOP_LEVEL_ROUTES) - .to include(*top_level_words), failure_block - end - end - - describe 'GROUP_ROUTES' do - it "don't contain a second wildcard" do - failure_block = lambda do - missing_words = paths_after_group_id - described_class::GROUP_ROUTES - failure_message(missing_words, 'GROUP_ROUTES', 'rename_child_paths') - end - - expect(described_class::GROUP_ROUTES) - .to include(*paths_after_group_id), failure_block - end - end - - describe 'PROJECT_WILDCARD_ROUTES' do - it 'includes all paths that can be used after a namespace/project path' do - aggregate_failures do - all_wildcard_paths.each do |path| - expect(wildcards_include?(path)) - .to be(true), failure_message(path, 'PROJECT_WILDCARD_ROUTES', 'rename_wildcard_paths') - end - end - end - end - - describe '.root_namespace_path_regex' do - subject { described_class.root_namespace_path_regex } - - it 'rejects top level routes' do - expect(subject).not_to match('admin/') - expect(subject).not_to match('api/') - expect(subject).not_to match('.well-known/') - end - - it 'accepts project wildcard routes' do - expect(subject).to match('blob/') - expect(subject).to match('edit/') - expect(subject).to match('wikis/') - end - - it 'accepts group routes' do - expect(subject).to match('activity/') - expect(subject).to match('group_members/') - expect(subject).to match('subgroups/') - end - - it 'is not case sensitive' do - expect(subject).not_to match('Users/') - end - - it 'does not allow extra slashes' do - expect(subject).not_to match('/blob/') - expect(subject).not_to match('blob//') - end - end - - describe '.full_namespace_path_regex' do - subject { described_class.full_namespace_path_regex } - - context 'at the top level' do - context 'when the final level' do - it 'rejects top level routes' do - expect(subject).not_to match('admin/') - expect(subject).not_to match('api/') - expect(subject).not_to match('.well-known/') - end - - it 'accepts project wildcard routes' do - expect(subject).to match('blob/') - expect(subject).to match('edit/') - expect(subject).to match('wikis/') - end - - it 'accepts group routes' do - expect(subject).to match('activity/') - expect(subject).to match('group_members/') - expect(subject).to match('subgroups/') - end - end - - context 'when more levels follow' do - it 'rejects top level routes' do - expect(subject).not_to match('admin/more/') - expect(subject).not_to match('api/more/') - expect(subject).not_to match('.well-known/more/') - end - - it 'accepts project wildcard routes' do - expect(subject).to match('blob/more/') - expect(subject).to match('edit/more/') - expect(subject).to match('wikis/more/') - expect(subject).to match('environments/folders/') - expect(subject).to match('info/lfs/objects/') - end - - it 'accepts group routes' do - expect(subject).to match('activity/more/') - expect(subject).to match('group_members/more/') - expect(subject).to match('subgroups/more/') - end - end - end - - context 'at the second level' do - context 'when the final level' do - it 'accepts top level routes' do - expect(subject).to match('root/admin/') - expect(subject).to match('root/api/') - expect(subject).to match('root/.well-known/') - end - - it 'rejects project wildcard routes' do - expect(subject).not_to match('root/blob/') - expect(subject).not_to match('root/edit/') - expect(subject).not_to match('root/wikis/') - expect(subject).not_to match('root/environments/folders/') - expect(subject).not_to match('root/info/lfs/objects/') - end - - it 'rejects group routes' do - expect(subject).not_to match('root/activity/') - expect(subject).not_to match('root/group_members/') - expect(subject).not_to match('root/subgroups/') - end - end - - context 'when more levels follow' do - it 'accepts top level routes' do - expect(subject).to match('root/admin/more/') - expect(subject).to match('root/api/more/') - expect(subject).to match('root/.well-known/more/') - end - - it 'rejects project wildcard routes' do - expect(subject).not_to match('root/blob/more/') - expect(subject).not_to match('root/edit/more/') - expect(subject).not_to match('root/wikis/more/') - expect(subject).not_to match('root/environments/folders/more/') - expect(subject).not_to match('root/info/lfs/objects/more/') - end - - it 'rejects group routes' do - expect(subject).not_to match('root/activity/more/') - expect(subject).not_to match('root/group_members/more/') - expect(subject).not_to match('root/subgroups/more/') - end - end - end - - it 'is not case sensitive' do - expect(subject).not_to match('root/Blob/') - end - - it 'does not allow extra slashes' do - expect(subject).not_to match('/root/admin/') - expect(subject).not_to match('root/admin//') - end - end - - describe '.project_path_regex' do - subject { described_class.project_path_regex } - - it 'accepts top level routes' do - expect(subject).to match('admin/') - expect(subject).to match('api/') - expect(subject).to match('.well-known/') - end - - it 'rejects project wildcard routes' do - expect(subject).not_to match('blob/') - expect(subject).not_to match('edit/') - expect(subject).not_to match('wikis/') - expect(subject).not_to match('environments/folders/') - expect(subject).not_to match('info/lfs/objects/') - end - - it 'accepts group routes' do - expect(subject).to match('activity/') - expect(subject).to match('group_members/') - expect(subject).to match('subgroups/') - end - - it 'is not case sensitive' do - expect(subject).not_to match('Blob/') - end - - it 'does not allow extra slashes' do - expect(subject).not_to match('/admin/') - expect(subject).not_to match('admin//') - end - end - - describe '.full_project_path_regex' do - subject { described_class.full_project_path_regex } - - it 'accepts top level routes' do - expect(subject).to match('root/admin/') - expect(subject).to match('root/api/') - expect(subject).to match('root/.well-known/') - end - - it 'rejects project wildcard routes' do - expect(subject).not_to match('root/blob/') - expect(subject).not_to match('root/edit/') - expect(subject).not_to match('root/wikis/') - expect(subject).not_to match('root/environments/folders/') - expect(subject).not_to match('root/info/lfs/objects/') - end - - it 'accepts group routes' do - expect(subject).to match('root/activity/') - expect(subject).to match('root/group_members/') - expect(subject).to match('root/subgroups/') - end - - it 'is not case sensitive' do - expect(subject).not_to match('root/Blob/') - end - - it 'does not allow extra slashes' do - expect(subject).not_to match('/root/admin/') - expect(subject).not_to match('root/admin//') - end - end - - describe '.namespace_regex' do - subject { described_class.namespace_regex } - - it { is_expected.to match('gitlab-ce') } - it { is_expected.to match('gitlab_git') } - it { is_expected.to match('_underscore.js') } - it { is_expected.to match('100px.com') } - it { is_expected.to match('gitlab.org') } - it { is_expected.not_to match('?gitlab') } - it { is_expected.not_to match('git lab') } - it { is_expected.not_to match('gitlab.git') } - it { is_expected.not_to match('gitlab.org.') } - it { is_expected.not_to match('gitlab.org/') } - it { is_expected.not_to match('/gitlab.org') } - it { is_expected.not_to match('gitlab git') } - end - - describe '.project_path_format_regex' do - subject { described_class.project_path_format_regex } - - it { is_expected.to match('gitlab-ce') } - it { is_expected.to match('gitlab_git') } - it { is_expected.to match('_underscore.js') } - it { is_expected.to match('100px.com') } - it { is_expected.not_to match('?gitlab') } - it { is_expected.not_to match('git lab') } - it { is_expected.not_to match('gitlab.git') } - end - describe '.project_name_regex' do subject { described_class.project_name_regex } @@ -412,16 +32,4 @@ describe Gitlab::Regex, lib: true do it { is_expected.not_to match('9foo') } it { is_expected.not_to match('foo-') } end - - describe '.full_namespace_regex' do - subject { described_class.full_namespace_regex } - - it { is_expected.to match('gitlab.org') } - it { is_expected.to match('gitlab.org/gitlab-git') } - it { is_expected.not_to match('gitlab.org.') } - it { is_expected.not_to match('gitlab.org/') } - it { is_expected.not_to match('/gitlab.org') } - it { is_expected.not_to match('gitlab.git') } - it { is_expected.not_to match('gitlab git') } - end end diff --git a/spec/lib/gitlab/sql/recursive_cte_spec.rb b/spec/lib/gitlab/sql/recursive_cte_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..25146860615091a988b61e42f99b6e2a99bb8461 --- /dev/null +++ b/spec/lib/gitlab/sql/recursive_cte_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Gitlab::SQL::RecursiveCTE, :postgresql do + let(:cte) { described_class.new(:cte_name) } + + describe '#to_arel' do + it 'generates an Arel relation for the CTE body' do + rel1 = User.where(id: 1) + rel2 = User.where(id: 2) + + cte << rel1 + cte << rel2 + + sql = cte.to_arel.to_sql + name = ActiveRecord::Base.connection.quote_table_name(:cte_name) + + sql1, sql2 = ActiveRecord::Base.connection.unprepared_statement do + [rel1.except(:order).to_sql, rel2.except(:order).to_sql] + end + + expect(sql).to eq("#{name} AS (#{sql1}\nUNION\n#{sql2})") + end + end + + describe '#alias_to' do + it 'returns an alias for the CTE' do + table = Arel::Table.new(:kittens) + + source_name = ActiveRecord::Base.connection.quote_table_name(:cte_name) + alias_name = ActiveRecord::Base.connection.quote_table_name(:kittens) + + expect(cte.alias_to(table).to_sql).to eq("#{source_name} AS #{alias_name}") + end + end + + describe '#apply_to' do + it 'applies a CTE to an ActiveRecord::Relation' do + user = create(:user) + cte = described_class.new(:cte_name) + + cte << User.where(id: user.id) + + relation = cte.apply_to(User.all) + + expect(relation.to_sql).to match(/WITH RECURSIVE.+cte_name/) + expect(relation.to_a).to eq(User.where(id: user.id).to_a) + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 1e6260270fe31c16576d6f4befed47d964c66972..ec6f6c42eacbeefa8209d38fd506ac9e3c18b70d 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -128,6 +128,15 @@ describe Notify do is_expected.to have_body_text(namespace_project_issue_path(project.namespace, project, issue)) end end + + context 'with a preferred language' do + before { Gitlab::I18n.locale = :es } + after { Gitlab::I18n.use_default_locale } + + it 'always generates the email using the default language' do + is_expected.to have_body_text('foo, bar, and baz') + end + end end describe 'status changed' do diff --git a/spec/migrations/fill_authorized_projects_spec.rb b/spec/migrations/fill_authorized_projects_spec.rb deleted file mode 100644 index 99dc41958188573a1cb15b3e99acecf667983456..0000000000000000000000000000000000000000 --- a/spec/migrations/fill_authorized_projects_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'spec_helper' -require Rails.root.join('db', 'post_migrate', '20170106142508_fill_authorized_projects.rb') - -describe FillAuthorizedProjects do - describe '#up' do - it 'schedules the jobs in batches' do - user1 = create(:user) - user2 = create(:user) - - expect(Sidekiq::Client).to receive(:push_bulk).with( - 'class' => 'AuthorizedProjectsWorker', - 'args' => [[user1.id], [user2.id]] - ) - - described_class.new.up - end - end -end diff --git a/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb b/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..175bf1876b2c207f7a598650481647047ff3dbed --- /dev/null +++ b/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb') + +describe TurnNestedGroupsIntoRegularGroupsForMysql do + let!(:parent_group) { create(:group) } + let!(:child_group) { create(:group, parent: parent_group) } + let!(:project) { create(:project, :empty_repo, namespace: child_group) } + let!(:member) { create(:user) } + let(:migration) { described_class.new } + + before do + parent_group.add_developer(member) + + allow(migration).to receive(:run_migration?).and_return(true) + allow(migration).to receive(:verbose).and_return(false) + end + + describe '#up' do + let(:updated_project) do + # path_with_namespace is memoized in an instance variable so we retrieve a + # new row here to work around that. + Project.find(project.id) + end + + before do + migration.up + end + + it 'unsets the parent_id column' do + expect(Namespace.where('parent_id IS NOT NULL').any?).to eq(false) + end + + it 'adds members of parent groups as members to the migrated group' do + is_member = child_group.members. + where(user_id: member, access_level: Gitlab::Access::DEVELOPER).any? + + expect(is_member).to eq(true) + end + + it 'update the path of the nested group' do + child_group.reload + + expect(child_group.path).to eq("#{parent_group.name}-#{child_group.name}") + end + + it 'renames projects of the nested group' do + expect(updated_project.path_with_namespace). + to eq("#{parent_group.name}-#{child_group.name}/#{updated_project.path}") + end + + it 'renames the repository of any projects' do + expect(updated_project.repository.path). + to end_with("#{parent_group.name}-#{child_group.name}/#{updated_project.path}.git") + + expect(File.directory?(updated_project.repository.path)).to eq(true) + end + + it 'creates a redirect route for renamed projects' do + exists = RedirectRoute. + where(source_type: 'Project', source_id: project.id). + any? + + expect(exists).to eq(true) + end + end +end diff --git a/spec/migrations/update_retried_for_ci_builds_spec.rb b/spec/migrations/update_retried_for_ci_build_spec.rb similarity index 100% rename from spec/migrations/update_retried_for_ci_builds_spec.rb rename to spec/migrations/update_retried_for_ci_build_spec.rb diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 822b98c5f6c9a40575d5d7f96b6c92e1df0f57af..b00e7a735712fb29396e7789e15e19d8bdd22651 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -25,6 +25,14 @@ describe Ci::PipelineSchedule, models: true do expect(pipeline_schedule).not_to be_valid end + + context 'when active is false' do + it 'does not allow nullified ref' do + pipeline_schedule = build(:ci_pipeline_schedule, :inactive, ref: nil) + + expect(pipeline_schedule).not_to be_valid + end + end end describe '#set_next_run_at' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 56b24ce62f3a724677e2c4c036f05e77e6a5434a..c8023dc13b18f7bdbba90dc787842ec257af7d91 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -965,7 +965,7 @@ describe Ci::Pipeline, models: true do end before do - ProjectWebHookWorker.drain + WebHookWorker.drain end context 'with pipeline hooks enabled' do diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index 49a4132f7638cf613f032dbc5b8ece87e93406be..0e10d91836d8964f49c573fac729c82944f93084 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -115,123 +115,6 @@ describe Group, 'Routable' do end end - describe '.member_descendants' do - let!(:user) { create(:user) } - let!(:nested_group) { create(:group, parent: group) } - - before { group.add_owner(user) } - subject { described_class.member_descendants(user.id) } - - it { is_expected.to eq([nested_group]) } - end - - describe '.member_self_and_descendants' do - let!(:user) { create(:user) } - let!(:nested_group) { create(:group, parent: group) } - - before { group.add_owner(user) } - subject { described_class.member_self_and_descendants(user.id) } - - it { is_expected.to match_array [group, nested_group] } - end - - describe '.member_hierarchy' do - # foo/bar would also match foo/barbaz instead of just foo/bar and foo/bar/baz - let!(:user) { create(:user) } - - # group - # _______ (foo) _______ - # | | - # | | - # nested_group_1 nested_group_2 - # (bar) (barbaz) - # | | - # | | - # nested_group_1_1 nested_group_2_1 - # (baz) (baz) - # - let!(:nested_group_1) { create :group, parent: group, name: 'bar' } - let!(:nested_group_1_1) { create :group, parent: nested_group_1, name: 'baz' } - let!(:nested_group_2) { create :group, parent: group, name: 'barbaz' } - let!(:nested_group_2_1) { create :group, parent: nested_group_2, name: 'baz' } - - context 'user is not a member of any group' do - subject { described_class.member_hierarchy(user.id) } - - it 'returns an empty array' do - is_expected.to eq [] - end - end - - context 'user is member of all groups' do - before do - group.add_owner(user) - nested_group_1.add_owner(user) - nested_group_1_1.add_owner(user) - nested_group_2.add_owner(user) - nested_group_2_1.add_owner(user) - end - subject { described_class.member_hierarchy(user.id) } - - it 'returns all groups' do - is_expected.to match_array [ - group, - nested_group_1, nested_group_1_1, - nested_group_2, nested_group_2_1 - ] - end - end - - context 'user is member of the top group' do - before { group.add_owner(user) } - subject { described_class.member_hierarchy(user.id) } - - it 'returns all groups' do - is_expected.to match_array [ - group, - nested_group_1, nested_group_1_1, - nested_group_2, nested_group_2_1 - ] - end - end - - context 'user is member of the first child (internal node), branch 1' do - before { nested_group_1.add_owner(user) } - subject { described_class.member_hierarchy(user.id) } - - it 'returns the groups in the hierarchy' do - is_expected.to match_array [ - group, - nested_group_1, nested_group_1_1 - ] - end - end - - context 'user is member of the first child (internal node), branch 2' do - before { nested_group_2.add_owner(user) } - subject { described_class.member_hierarchy(user.id) } - - it 'returns the groups in the hierarchy' do - is_expected.to match_array [ - group, - nested_group_2, nested_group_2_1 - ] - end - end - - context 'user is member of the last child (leaf node)' do - before { nested_group_1_1.add_owner(user) } - subject { described_class.member_hierarchy(user.id) } - - it 'returns the groups in the hierarchy' do - is_expected.to match_array [ - group, - nested_group_1, nested_group_1_1 - ] - end - end - end - describe '#full_path' do let(:group) { create(:group) } let(:nested_group) { create(:group, parent: group) } diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index ab4c51a87b0eea4a8c21a5a7d0eb71c20e0d41c2..96f075d4f7d52780df32b4287ea563aefa6f902c 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -145,7 +145,7 @@ describe DiffNote, models: true do context "when the merge request's diff refs don't match that of the diff note" do before do - allow(subject.noteable).to receive(:diff_sha_refs).and_return(commit.diff_refs) + allow(subject.noteable).to receive(:diff_refs).and_return(commit.diff_refs) end it "returns false" do @@ -194,7 +194,7 @@ describe DiffNote, models: true do context "when the note is outdated" do before do - allow(merge_request).to receive(:diff_sha_refs).and_return(commit.diff_refs) + allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs) end it "uses the DiffPositionUpdateService" do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 6ca1eb0374d32601357c7c76e93fb2bf26a09e1f..316bf153660ffa815907f544d4c973e44eed6bc9 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -340,7 +340,7 @@ describe Group, models: true do it { expect(subject.parent).to be_kind_of(Group) } end - describe '#members_with_parents' do + describe '#members_with_parents', :nested_groups do let!(:group) { create(:group, :nested) } let!(:master) { group.parent.add_user(create(:user), GroupMember::MASTER) } let!(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) } diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb index 1a83c836652ebd2c15667f64f19511872f4b8e15..57454d2a773c4fe248c88d02c200f3de4eb860af 100644 --- a/spec/models/hooks/service_hook_spec.rb +++ b/spec/models/hooks/service_hook_spec.rb @@ -1,36 +1,19 @@ -require "spec_helper" +require 'spec_helper' describe ServiceHook, models: true do - describe "Associations" do + describe 'associations' do it { is_expected.to belong_to :service } end - describe "execute" do - before(:each) do - @service_hook = create(:service_hook) - @data = { project_id: 1, data: {} } + describe 'execute' do + let(:hook) { build(:service_hook) } + let(:data) { { key: 'value' } } - WebMock.stub_request(:post, @service_hook.url) - end - - it "POSTs to the webhook URL" do - @service_hook.execute(@data) - expect(WebMock).to have_requested(:post, @service_hook.url).with( - headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Service Hook' } - ).once - end - - it "POSTs the data as JSON" do - @service_hook.execute(@data) - expect(WebMock).to have_requested(:post, @service_hook.url).with( - headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Service Hook' } - ).once - end - - it "catches exceptions" do - expect(WebHook).to receive(:post).and_raise("Some HTTP Post error") + it '#execute' do + expect(WebHookService).to receive(:new).with(hook, data, 'service_hook').and_call_original + expect_any_instance_of(WebHookService).to receive(:execute) - expect { @service_hook.execute(@data) }.to raise_error(RuntimeError) + hook.execute(data) end end end diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 4340170888dd5d0cdda6315b15f3808059d2f434..0d2b622132e70b8bf3ffd3f0cbef20d7449efa05 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -126,4 +126,26 @@ describe SystemHook, models: true do expect(SystemHook.repository_update_hooks).to eq([hook]) end end + + describe 'execute WebHookService' do + let(:hook) { build(:system_hook) } + let(:data) { { key: 'value' } } + let(:hook_name) { 'system_hook' } + + before do + expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original + end + + it '#execute' do + expect_any_instance_of(WebHookService).to receive(:execute) + + hook.execute(data, hook_name) + end + + it '#async_execute' do + expect_any_instance_of(WebHookService).to receive(:async_execute) + + hook.async_execute(data, hook_name) + end + end end diff --git a/spec/models/hooks/web_hook_log_spec.rb b/spec/models/hooks/web_hook_log_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c649cf3b589e58d5b22dfe036977322b422ba782 --- /dev/null +++ b/spec/models/hooks/web_hook_log_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +describe WebHookLog, models: true do + it { is_expected.to belong_to(:web_hook) } + + it { is_expected.to serialize(:request_headers).as(Hash) } + it { is_expected.to serialize(:request_data).as(Hash) } + it { is_expected.to serialize(:response_headers).as(Hash) } + + it { is_expected.to validate_presence_of(:web_hook) } + + describe '#success?' do + let(:web_hook_log) { build(:web_hook_log, response_status: status) } + + describe '2xx' do + let(:status) { '200' } + it { expect(web_hook_log.success?).to be_truthy } + end + + describe 'not 2xx' do + let(:status) { '500' } + it { expect(web_hook_log.success?).to be_falsey } + end + + describe 'internal erorr' do + let(:status) { 'internal error' } + it { expect(web_hook_log.success?).to be_falsey } + end + end +end diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 9d4db1bfb526ae0a437928e8f2df47028bffcece..53157c244770b7b5b92698e56c2df6f9ba87448b 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -1,89 +1,54 @@ require 'spec_helper' describe WebHook, models: true do - describe "Validations" do + let(:hook) { build(:project_hook) } + + describe 'associations' do + it { is_expected.to have_many(:web_hook_logs).dependent(:destroy) } + end + + describe 'validations' do it { is_expected.to validate_presence_of(:url) } describe 'url' do - it { is_expected.to allow_value("http://example.com").for(:url) } - it { is_expected.to allow_value("https://example.com").for(:url) } - it { is_expected.to allow_value(" https://example.com ").for(:url) } - it { is_expected.to allow_value("http://test.com/api").for(:url) } - it { is_expected.to allow_value("http://test.com/api?key=abc").for(:url) } - it { is_expected.to allow_value("http://test.com/api?key=abc&type=def").for(:url) } + it { is_expected.to allow_value('http://example.com').for(:url) } + it { is_expected.to allow_value('https://example.com').for(:url) } + it { is_expected.to allow_value(' https://example.com ').for(:url) } + it { is_expected.to allow_value('http://test.com/api').for(:url) } + it { is_expected.to allow_value('http://test.com/api?key=abc').for(:url) } + it { is_expected.to allow_value('http://test.com/api?key=abc&type=def').for(:url) } - it { is_expected.not_to allow_value("example.com").for(:url) } - it { is_expected.not_to allow_value("ftp://example.com").for(:url) } - it { is_expected.not_to allow_value("herp-and-derp").for(:url) } + it { is_expected.not_to allow_value('example.com').for(:url) } + it { is_expected.not_to allow_value('ftp://example.com').for(:url) } + it { is_expected.not_to allow_value('herp-and-derp').for(:url) } it 'strips :url before saving it' do - hook = create(:project_hook, url: ' https://example.com ') + hook.url = ' https://example.com ' + hook.save expect(hook.url).to eq('https://example.com') end end end - describe "execute" do - let(:project) { create(:empty_project) } - let(:project_hook) { create(:project_hook) } - - before(:each) do - project.hooks << [project_hook] - @data = { before: 'oldrev', after: 'newrev', ref: 'ref' } - - WebMock.stub_request(:post, project_hook.url) - end - - context 'when token is defined' do - let(:project_hook) { create(:project_hook, :token) } - - it 'POSTs to the webhook URL' do - project_hook.execute(@data, 'push_hooks') - expect(WebMock).to have_requested(:post, project_hook.url).with( - headers: { 'Content-Type' => 'application/json', - 'X-Gitlab-Event' => 'Push Hook', - 'X-Gitlab-Token' => project_hook.token } - ).once - end - end - - it "POSTs to the webhook URL" do - project_hook.execute(@data, 'push_hooks') - expect(WebMock).to have_requested(:post, project_hook.url).with( - headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Push Hook' } - ).once - end - - it "POSTs the data as JSON" do - project_hook.execute(@data, 'push_hooks') - expect(WebMock).to have_requested(:post, project_hook.url).with( - headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Push Hook' } - ).once - end - - it "catches exceptions" do - expect(WebHook).to receive(:post).and_raise("Some HTTP Post error") - - expect { project_hook.execute(@data, 'push_hooks') }.to raise_error(RuntimeError) - end - - it "handles SSL exceptions" do - expect(WebHook).to receive(:post).and_raise(OpenSSL::SSL::SSLError.new('SSL error')) + describe 'execute' do + let(:data) { { key: 'value' } } + let(:hook_name) { 'project hook' } - expect(project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error']) + before do + expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original end - it "handles 200 status code" do - WebMock.stub_request(:post, project_hook.url).to_return(status: 200, body: "Success") + it '#execute' do + expect_any_instance_of(WebHookService).to receive(:execute) - expect(project_hook.execute(@data, 'push_hooks')).to eq([200, 'Success']) + hook.execute(data, hook_name) end - it "handles 2xx status codes" do - WebMock.stub_request(:post, project_hook.url).to_return(status: 201, body: "Success") + it '#async_execute' do + expect_any_instance_of(WebHookService).to receive(:async_execute) - expect(project_hook.execute(@data, 'push_hooks')).to eq([201, 'Success']) + hook.async_execute(data, hook_name) end end end diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 80ca19acddae6a8255261680883e067dca6fb26e..84867e3d96b1fcc7a6f928d8efb8cdfa5c7e7548 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -49,6 +49,23 @@ describe Label, models: true do expect(label.color).to eq('#abcdef') end + + it 'uses default color if color is missing' do + label = described_class.new(color: nil) + + expect(label.color).to be(Label::DEFAULT_COLOR) + end + end + + describe '#text_color' do + it 'uses default color if color is missing' do + expect(LabelsHelper).to receive(:text_color_for_bg).with(Label::DEFAULT_COLOR). + and_return(spy) + + label = described_class.new(color: nil) + + label.text_color + end end describe '#title' do diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 87ea2e706807d39431b2d90ecc8c4129feb43bcf..cf9c701e8c580015cd0ae1a3b04e586b21f1ca48 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -22,16 +22,15 @@ describe ProjectMember, models: true do end describe '.add_user' do - context 'when called with the project owner' do - it 'adds the user as a member' do - project = create(:empty_project) + it 'adds the user as a member' do + user = create(:user) + project = create(:empty_project) - expect(project.users).not_to include(project.owner) + expect(project.users).not_to include(user) - described_class.add_user(project, project.owner, :master, current_user: project.owner) + described_class.add_user(project, user, :master, current_user: project.owner) - expect(project.users.reload).to include(project.owner) - end + expect(project.users.reload).to include(user) end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index da915c49d3c09bb124e7f1b75bca1224a8868e15..712470d6bf5914004f02fcebe0798840c72125bd 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1243,7 +1243,7 @@ describe MergeRequest, models: true do end end - describe "#diff_sha_refs" do + describe "#diff_refs" do context "with diffs" do subject { create(:merge_request, :with_diffs) } @@ -1252,7 +1252,7 @@ describe MergeRequest, models: true do expect_any_instance_of(Repository).not_to receive(:commit) - subject.diff_sha_refs + subject.diff_refs end it "returns expected diff_refs" do @@ -1262,7 +1262,7 @@ describe MergeRequest, models: true do head_sha: subject.merge_request_diff.head_commit_sha ) - expect(subject.diff_sha_refs).to eq(expected_diff_refs) + expect(subject.diff_refs).to eq(expected_diff_refs) end end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index e3e8e6d571cb9bfd438e5d6e6e0bbdf1d768c652..aa1ce89ffd7f713ad7d7c43f20b7b33fe8df056d 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -249,4 +249,17 @@ describe Milestone, models: true do expect(milestone.to_reference(another_project)).to eq "sample-project%1" end end + + describe '#participants' do + let(:project) { build(:empty_project, name: 'sample-project') } + let(:milestone) { build(:milestone, iid: 1, project: project) } + + it 'returns participants without duplicates' do + user = create :user + create :issue, project: project, milestone: milestone, assignees: [user] + create :issue, project: project, milestone: milestone, assignees: [user] + + expect(milestone.participants).to eq [user] + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 312302afdbb6c0b3a15e038e30a48a457069b358..0e74f1ab1bd104604b6ed9fb390c623a06d4a37a 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -238,8 +238,8 @@ describe Namespace, models: true do end context 'in sub-groups' do - let(:parent) { create(:namespace, path: 'parent') } - let(:child) { create(:namespace, parent: parent, path: 'child') } + let(:parent) { create(:group, path: 'parent') } + let(:child) { create(:group, parent: parent, path: 'child') } let!(:project) { create(:project_empty_repo, namespace: child) } let(:path_in_dir) { File.join(repository_storage_path, 'parent', 'child') } let(:deleted_path) { File.join('parent', "child+#{child.id}+deleted") } @@ -287,21 +287,21 @@ describe Namespace, models: true do end end - describe '#ancestors' do + describe '#ancestors', :nested_groups do let(:group) { create(:group) } let(:nested_group) { create(:group, parent: group) } let(:deep_nested_group) { create(:group, parent: nested_group) } let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } it 'returns the correct ancestors' do - expect(very_deep_nested_group.ancestors).to eq([group, nested_group, deep_nested_group]) - expect(deep_nested_group.ancestors).to eq([group, nested_group]) - expect(nested_group.ancestors).to eq([group]) + expect(very_deep_nested_group.ancestors).to include(group, nested_group, deep_nested_group) + expect(deep_nested_group.ancestors).to include(group, nested_group) + expect(nested_group.ancestors).to include(group) expect(group.ancestors).to eq([]) end end - describe '#descendants' do + describe '#descendants', :nested_groups do let!(:group) { create(:group, path: 'git_lab') } let!(:nested_group) { create(:group, parent: group) } let!(:deep_nested_group) { create(:group, parent: nested_group) } @@ -311,9 +311,9 @@ describe Namespace, models: true do it 'returns the correct descendants' do expect(very_deep_nested_group.descendants.to_a).to eq([]) - expect(deep_nested_group.descendants.to_a).to eq([very_deep_nested_group]) - expect(nested_group.descendants.to_a).to eq([deep_nested_group, very_deep_nested_group]) - expect(group.descendants.to_a).to eq([nested_group, deep_nested_group, very_deep_nested_group]) + expect(deep_nested_group.descendants.to_a).to include(very_deep_nested_group) + expect(nested_group.descendants.to_a).to include(deep_nested_group, very_deep_nested_group) + expect(group.descendants.to_a).to include(nested_group, deep_nested_group, very_deep_nested_group) end end diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb index 9b711bfc007501e0c7ec34e14716f567fb1aa595..4161b9158b1ac6693a50e8a1d003ff51cb844485 100644 --- a/spec/models/project_group_link_spec.rb +++ b/spec/models/project_group_link_spec.rb @@ -23,7 +23,7 @@ describe ProjectGroupLink do expect(project_group_link).not_to be_valid end - it "doesn't allow a project to be shared with an ancestor of the group it is in" do + it "doesn't allow a project to be shared with an ancestor of the group it is in", :nested_groups do project_group_link.group = parent_group expect(project_group_link).not_to be_valid diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 4bca0229e7aee3eab66dc1cc2e36c760df3e18d3..349067e73ab6ab6062994d5caab0edb16c71b334 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -22,6 +22,42 @@ describe JiraService, models: true do it { is_expected.not_to validate_presence_of(:url) } end + + context 'validating urls' do + let(:service) do + described_class.new( + project: create(:empty_project), + active: true, + username: 'username', + password: 'test', + project_key: 'TEST', + jira_issue_transition_id: 24, + url: 'http://jira.test.com' + ) + end + + it 'is valid when all fields have required values' do + expect(service).to be_valid + end + + it 'is not valid when url is not a valid url' do + service.url = 'not valid' + + expect(service).not_to be_valid + end + + it 'is not valid when api url is not a valid url' do + service.api_url = 'not valid' + + expect(service).not_to be_valid + end + + it 'is valid when api url is a valid url' do + service.api_url = 'http://jira.test.com/api' + + expect(service).to be_valid + end + end end describe '#reference_pattern' do @@ -187,22 +223,29 @@ describe JiraService, models: true do describe '#test_settings' do let(:jira_service) do described_class.new( + project: create(:project), url: 'http://jira.example.com', - username: 'gitlab_jira_username', - password: 'gitlab_jira_password', + username: 'jira_username', + password: 'jira_password', project_key: 'GitLabProject' ) end - let(:project_url) { 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/project/GitLabProject' } - before do + def test_settings(api_url) + project_url = "http://jira_username:jira_password@#{api_url}/rest/api/2/project/GitLabProject" + WebMock.stub_request(:get, project_url) - end - it 'tries to get JIRA project' do jira_service.test_settings + end + + it 'tries to get JIRA project with URL when API URL not set' do + test_settings('jira.example.com') + end - expect(WebMock).to have_requested(:get, project_url) + it 'tries to get JIRA project with API URL if set' do + jira_service.update(api_url: 'http://jira.api.com') + test_settings('jira.api.com') end end @@ -214,34 +257,75 @@ describe JiraService, models: true do @jira_service = JiraService.create!( project: project, properties: { - url: 'http://jira.example.com/rest/api/2', + url: 'http://jira.example.com/web', username: 'mic', password: "password" } ) end - it "reset password if url changed" do - @jira_service.url = 'http://jira_edited.example.com/rest/api/2' - @jira_service.save - expect(@jira_service.password).to be_nil + context 'when only web url present' do + it 'reset password if url changed' do + @jira_service.url = 'http://jira_edited.example.com/rest/api/2' + @jira_service.save + + expect(@jira_service.password).to be_nil + end + + it 'reset password if url not changed but api url added' do + @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2' + @jira_service.save + + expect(@jira_service.password).to be_nil + end end - it "does not reset password if username changed" do - @jira_service.username = "some_name" + context 'when both web and api url present' do + before do + @jira_service.api_url = 'http://jira.example.com/rest/api/2' + @jira_service.password = 'password' + + @jira_service.save + end + it 'reset password if api url changed' do + @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2' + @jira_service.save + + expect(@jira_service.password).to be_nil + end + + it 'does not reset password if url changed' do + @jira_service.url = 'http://jira_edited.example.com/rweb' + @jira_service.save + + expect(@jira_service.password).to eq("password") + end + + it 'reset password if api url set to ""' do + @jira_service.api_url = '' + @jira_service.save + + expect(@jira_service.password).to be_nil + end + end + + it 'does not reset password if username changed' do + @jira_service.username = 'some_name' @jira_service.save - expect(@jira_service.password).to eq("password") + + expect(@jira_service.password).to eq('password') end - it "does not reset password if new url is set together with password, even if it's the same password" do + it 'does not reset password if new url is set together with password, even if it\'s the same password' do @jira_service.url = 'http://jira_edited.example.com/rest/api/2' @jira_service.password = 'password' @jira_service.save - expect(@jira_service.password).to eq("password") - expect(@jira_service.url).to eq("http://jira_edited.example.com/rest/api/2") + + expect(@jira_service.password).to eq('password') + expect(@jira_service.url).to eq('http://jira_edited.example.com/rest/api/2') end - it "resets password if url changed, even if setter called multiple times" do + it 'resets password if url changed, even if setter called multiple times' do @jira_service.url = 'http://jira1.example.com/rest/api/2' @jira_service.url = 'http://jira1.example.com/rest/api/2' @jira_service.save @@ -249,7 +333,7 @@ describe JiraService, models: true do end end - context "when no password was previously set" do + context 'when no password was previously set' do before do @jira_service = JiraService.create( project: project, @@ -260,26 +344,16 @@ describe JiraService, models: true do ) end - it "saves password if new url is set together with password" do + it 'saves password if new url is set together with password' do @jira_service.url = 'http://jira_edited.example.com/rest/api/2' @jira_service.password = 'password' @jira_service.save - expect(@jira_service.password).to eq("password") - expect(@jira_service.url).to eq("http://jira_edited.example.com/rest/api/2") + expect(@jira_service.password).to eq('password') + expect(@jira_service.url).to eq('http://jira_edited.example.com/rest/api/2') end end end - describe "Validations" do - context "active" do - before do - subject.active = true - end - - it { is_expected.to validate_presence_of :url } - end - end - describe 'description and title' do let(:project) { create(:empty_project) } @@ -321,9 +395,10 @@ describe JiraService, models: true do context 'when gitlab.yml was initialized' do before do settings = { - "jira" => { - "title" => "Jira", - "url" => "http://jira.sample/projects/project_a" + 'jira' => { + 'title' => 'Jira', + 'url' => 'http://jira.sample/projects/project_a', + 'api_url' => 'http://jira.sample/api' } } allow(Gitlab.config).to receive(:issues_tracker).and_return(settings) @@ -335,8 +410,9 @@ describe JiraService, models: true do end it 'is prepopulated with the settings' do - expect(@service.properties["title"]).to eq('Jira') - expect(@service.properties["url"]).to eq('http://jira.sample/projects/project_a') + expect(@service.properties['title']).to eq('Jira') + expect(@service.properties['url']).to eq('http://jira.sample/projects/project_a') + expect(@service.properties['api_url']).to eq('http://jira.sample/api') end end end diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index c1c2f2a721982ba08307372e3211c88a478a09f6..0dcf4a4b5d62e0967360e393fb2d333997b6d11a 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -13,7 +13,7 @@ describe KubernetesService, models: true, caching: true do let(:discovery_url) { service.api_url + '/api/v1' } let(:discovery_response) { { body: kube_discovery_body.to_json } } - let(:pods_url) { service.api_url + "/api/v1/namespaces/#{service.namespace}/pods" } + let(:pods_url) { service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods" } let(:pods_response) { { body: kube_pods_body(kube_pod).to_json } } def stub_kubeclient_discover @@ -100,7 +100,35 @@ describe KubernetesService, models: true, caching: true do it 'sets the namespace to the default' do expect(kube_namespace).not_to be_nil - expect(kube_namespace[:placeholder]).to match(/\A#{Gitlab::Regex::PATH_REGEX_STR}-\d+\z/) + expect(kube_namespace[:placeholder]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/) + end + end + end + + describe '#actual_namespace' do + subject { service.actual_namespace } + + it "returns the default namespace" do + is_expected.to eq(service.send(:default_namespace)) + end + + context 'when namespace is specified' do + before do + service.namespace = 'my-namespace' + end + + it "returns the user-namespace" do + is_expected.to eq('my-namespace') + end + end + + context 'when service is not assigned to project' do + before do + service.project = nil + end + + it "does not return namespace" do + is_expected.to be_nil end end end @@ -187,13 +215,14 @@ describe KubernetesService, models: true, caching: true do kube_namespace = subject.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' } expect(kube_namespace).not_to be_nil - expect(kube_namespace[:value]).to match(/\A#{Gitlab::Regex::PATH_REGEX_STR}-\d+\z/) + expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/) end end end describe '#terminals' do let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") } + subject { service.terminals(environment) } context 'with invalid pods' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f2b4e9070b41592dc2dc2e30b90d82a26ecb187b..36575acf671f470724c9dc8273bc4d6e671ca11a 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1431,6 +1431,31 @@ describe Project, models: true do end end + describe 'Project import job' do + let(:project) { create(:empty_project) } + let(:mirror) { false } + + before do + allow_any_instance_of(Gitlab::Shell).to receive(:import_repository) + .with(project.repository_storage_path, project.path_with_namespace, project.import_url) + .and_return(true) + + allow(project).to receive(:repository_exists?).and_return(true) + + expect_any_instance_of(Repository).to receive(:after_import) + .and_call_original + end + + it 'imports a project' do + expect_any_instance_of(RepositoryImportWorker).to receive(:perform).and_call_original + + project.import_start + project.add_import_job + + expect(project.reload.import_status).to eq('finished') + end + end + describe '#latest_successful_builds_for' do def create_pipeline(status = 'success') create(:ci_pipeline, project: project, diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 942eeab251daad845926c655ec84f3c064461d6c..fb2d5f600098e9167bebe9239d85a2eb19f7b5c6 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -81,7 +81,7 @@ describe ProjectTeam, models: true do user = create(:user) project.add_guest(user) - expect(project.team.members).to contain_exactly(user) + expect(project.team.members).to contain_exactly(user, project.owner) end it 'returns project members of a specified level' do @@ -100,7 +100,8 @@ describe ProjectTeam, models: true do group_access: Gitlab::Access::GUEST ) - expect(project.team.members).to contain_exactly(group_member.user) + expect(project.team.members). + to contain_exactly(group_member.user, project.owner) end it 'returns invited members of a group of a specified level' do @@ -137,7 +138,10 @@ describe ProjectTeam, models: true do describe '#find_member' do context 'personal project' do - let(:project) { create(:empty_project, :public, :access_requestable) } + let(:project) do + create(:empty_project, :public, :access_requestable) + end + let(:requester) { create(:user) } before do @@ -200,7 +204,9 @@ describe ProjectTeam, models: true do let(:requester) { create(:user) } context 'personal project' do - let(:project) { create(:empty_project, :public, :access_requestable) } + let(:project) do + create(:empty_project, :public, :access_requestable) + end context 'when project is not shared with group' do before do @@ -244,7 +250,9 @@ describe ProjectTeam, models: true do context 'group project' do let(:group) { create(:group, :access_requestable) } - let!(:project) { create(:empty_project, group: group) } + let!(:project) do + create(:empty_project, group: group) + end before do group.add_master(master) @@ -265,8 +273,15 @@ describe ProjectTeam, models: true do let(:group) { create(:group) } let(:developer) { create(:user) } let(:master) { create(:user) } - let(:personal_project) { create(:empty_project, namespace: developer.namespace) } - let(:group_project) { create(:empty_project, namespace: group) } + + let(:personal_project) do + create(:empty_project, namespace: developer.namespace) + end + + let(:group_project) do + create(:empty_project, namespace: group) + end + let(:members_project) { create(:empty_project) } let(:shared_project) { create(:empty_project) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index aabdac4bb7562bb43300be768f2c14431c3ef602..9edf34b05ad3a1d9ed9670a4edfab43b785dfaaf 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -627,16 +627,6 @@ describe User, models: true do it { expect(User.without_projects).to include user_without_project2 } end - describe '.not_in_project' do - before do - User.delete_all - @user = create :user - @project = create(:empty_project) - end - - it { expect(User.not_in_project(@project)).to include(@user, @project.owner) } - end - describe 'user creation' do describe 'normal user' do let(:user) { create(:user, name: 'John Smith') } @@ -1561,48 +1551,103 @@ describe User, models: true do end end - describe '#nested_groups' do + describe '#all_expanded_groups' do + # foo/bar would also match foo/barbaz instead of just foo/bar and foo/bar/baz let!(:user) { create(:user) } - let!(:group) { create(:group) } - let!(:nested_group) { create(:group, parent: group) } - before do - group.add_owner(user) + # group + # _______ (foo) _______ + # | | + # | | + # nested_group_1 nested_group_2 + # (bar) (barbaz) + # | | + # | | + # nested_group_1_1 nested_group_2_1 + # (baz) (baz) + # + let!(:group) { create :group } + let!(:nested_group_1) { create :group, parent: group, name: 'bar' } + let!(:nested_group_1_1) { create :group, parent: nested_group_1, name: 'baz' } + let!(:nested_group_2) { create :group, parent: group, name: 'barbaz' } + let!(:nested_group_2_1) { create :group, parent: nested_group_2, name: 'baz' } - # Add more data to ensure method does not include wrong groups - create(:group).add_owner(create(:user)) + subject { user.all_expanded_groups } + + context 'user is not a member of any group' do + it 'returns an empty array' do + is_expected.to eq([]) + end end - it { expect(user.nested_groups).to eq([nested_group]) } - end + context 'user is member of all groups' do + before do + group.add_owner(user) + nested_group_1.add_owner(user) + nested_group_1_1.add_owner(user) + nested_group_2.add_owner(user) + nested_group_2_1.add_owner(user) + end - describe '#all_expanded_groups' do - let!(:user) { create(:user) } - let!(:group) { create(:group) } - let!(:nested_group_1) { create(:group, parent: group) } - let!(:nested_group_2) { create(:group, parent: group) } + it 'returns all groups' do + is_expected.to match_array [ + group, + nested_group_1, nested_group_1_1, + nested_group_2, nested_group_2_1 + ] + end + end - before { nested_group_1.add_owner(user) } + context 'user is member of the top group' do + before { group.add_owner(user) } - it { expect(user.all_expanded_groups).to match_array [group, nested_group_1] } - end + if Group.supports_nested_groups? + it 'returns all groups' do + is_expected.to match_array [ + group, + nested_group_1, nested_group_1_1, + nested_group_2, nested_group_2_1 + ] + end + else + it 'returns the top-level groups' do + is_expected.to match_array [group] + end + end + end - describe '#nested_groups_projects' do - let!(:user) { create(:user) } - let!(:group) { create(:group) } - let!(:nested_group) { create(:group, parent: group) } - let!(:project) { create(:empty_project, namespace: group) } - let!(:nested_project) { create(:empty_project, namespace: nested_group) } + context 'user is member of the first child (internal node), branch 1', :nested_groups do + before { nested_group_1.add_owner(user) } - before do - group.add_owner(user) + it 'returns the groups in the hierarchy' do + is_expected.to match_array [ + group, + nested_group_1, nested_group_1_1 + ] + end + end + + context 'user is member of the first child (internal node), branch 2', :nested_groups do + before { nested_group_2.add_owner(user) } - # Add more data to ensure method does not include wrong projects - other_project = create(:empty_project, namespace: create(:group, :nested)) - other_project.add_developer(create(:user)) + it 'returns the groups in the hierarchy' do + is_expected.to match_array [ + group, + nested_group_2, nested_group_2_1 + ] + end end - it { expect(user.nested_groups_projects).to eq([nested_project]) } + context 'user is member of the last child (leaf node)', :nested_groups do + before { nested_group_1_1.add_owner(user) } + + it 'returns the groups in the hierarchy' do + is_expected.to match_array [ + group, + nested_group_1, nested_group_1_1 + ] + end + end end describe '#refresh_authorized_projects', redis: true do @@ -1622,10 +1667,6 @@ describe User, models: true do expect(user.project_authorizations.count).to eq(2) end - it 'sets the authorized_projects_populated column' do - expect(user.authorized_projects_populated).to eq(true) - end - it 'stores the correct access levels' do expect(user.project_authorizations.where(access_level: Gitlab::Access::GUEST).exists?).to eq(true) expect(user.project_authorizations.where(access_level: Gitlab::Access::REPORTER).exists?).to eq(true) @@ -1735,7 +1776,7 @@ describe User, models: true do end end - context 'with 2FA requirement on nested parent group' do + context 'with 2FA requirement on nested parent group', :nested_groups do let!(:group1) { create :group, require_two_factor_authentication: true } let!(:group1a) { create :group, require_two_factor_authentication: false, parent: group1 } @@ -1750,7 +1791,7 @@ describe User, models: true do end end - context 'with 2FA requirement on nested child group' do + context 'with 2FA requirement on nested child group', :nested_groups do let!(:group1) { create :group, require_two_factor_authentication: false } let!(:group1a) { create :group, require_two_factor_authentication: true, parent: group1 } diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 2077c14ff7a3c15c1a680f1b242f95f12e3a375d..4c37a553227862bfebd19a26fcf0a8a220375c2b 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -107,7 +107,7 @@ describe GroupPolicy, models: true do end end - describe 'private nested group inherit permissions' do + describe 'private nested group inherit permissions', :nested_groups do let(:nested_group) { create(:group, :private, parent: group) } subject { described_class.abilities(current_user, nested_group).to_set } diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 0b0e4c2b1126bcf54ce6933d872ba3f07ac9b613..b84361d3abd519717ea8d1699d6ef6c581929239 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -5,7 +5,6 @@ describe API::Commits do let(:user) { create(:user) } let(:user2) { create(:user) } let!(:project) { create(:project, :repository, creator: user, namespace: user.namespace) } - let!(:master) { create(:project_member, :master, user: user, project: project) } let!(:guest) { create(:project_member, :guest, user: user2, project: project) } let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') } diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 90b36374ded161c07219caba5f0d4ff39a165705..bb53796cbd7c63214c82bae2e829b4c0217ac985 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -429,7 +429,7 @@ describe API::Groups do expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled]) end - it "creates a nested group" do + it "creates a nested group", :nested_groups do parent = create(:group) parent.add_owner(user3) group = attributes_for(:group, { parent_id: parent.id }) diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index f9e5316b3de5c9bb0f78a4eef6c11c79c60da223..9e6957e99225f731910a9108d6bc7955542ef055 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -7,7 +7,7 @@ describe API::Pipelines do let!(:pipeline) do create(:ci_empty_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch) + ref: project.default_branch, user: user) end before { project.team << [user, :master] } @@ -232,20 +232,26 @@ describe API::Pipelines do context 'when order_by and sort are specified' do context 'when order_by user_id' do - let!(:pipeline) { create_list(:ci_pipeline, 2, project: project, user: create(:user)) } + before do + 3.times do + create(:ci_pipeline, project: project, user: create(:user)) + end + end - it 'sorts as user_id: :asc' do - get api("/projects/#{project.id}/pipelines", user), order_by: 'user_id', sort: 'asc' + context 'when sort parameter is valid' do + it 'sorts as user_id: :desc' do + get api("/projects/#{project.id}/pipelines", user), order_by: 'user_id', sort: 'desc' - expect(response).to have_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).not_to be_empty - pipeline.sort_by { |p| p.user.id }.tap do |sorted_pipeline| - json_response.each_with_index { |r, i| expect(r['id']).to eq(sorted_pipeline[i].id) } + expect(response).to have_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).not_to be_empty + + pipeline_ids = Ci::Pipeline.all.order(user_id: :desc).pluck(:id) + expect(json_response.map { |r| r['id'] }).to eq(pipeline_ids) end end - context 'when sort is invalid' do + context 'when sort parameter is invalid' do it 'returns bad_request' do get api("/projects/#{project.id}/pipelines", user), order_by: 'user_id', sort: 'invalid_sort' diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index d5c3b5b34adc606b8807c7d711acbfe89eeadcb3..f95a287a184afb11c792ff3bb260ac4457ec9341 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -11,8 +11,7 @@ describe API::Projects do let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } let(:project2) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace) } let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } - let(:project_member) { create(:project_member, :master, user: user, project: project) } - let(:project_member2) { create(:project_member, :developer, user: user3, project: project) } + let(:project_member) { create(:project_member, :developer, user: user3, project: project) } let(:user4) { create(:user) } let(:project3) do create(:project, @@ -27,7 +26,7 @@ describe API::Projects do builds_enabled: false, snippets_enabled: false) end - let(:project_member3) do + let(:project_member2) do create(:project_member, user: user4, project: project3, @@ -210,7 +209,7 @@ describe API::Projects do let(:public_project) { create(:empty_project, :public) } before do - project_member2 + project_member user3.update_attributes(starred_projects: [project, project2, project3, public_project]) end @@ -784,19 +783,18 @@ describe API::Projects do describe 'GET /projects/:id/users' do shared_examples_for 'project users response' do it 'returns the project users' do - member = create(:user) - create(:project_member, :developer, user: member, project: project) - get api("/projects/#{project.id}/users", current_user) + user = project.namespace.owner + expect(response).to have_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) first_user = json_response.first - expect(first_user['username']).to eq(member.username) - expect(first_user['name']).to eq(member.name) + expect(first_user['username']).to eq(user.username) + expect(first_user['name']).to eq(user.name) expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url]) end end @@ -1091,8 +1089,8 @@ describe API::Projects do before { user4 } before { project3 } before { project4 } - before { project_member3 } before { project_member2 } + before { project_member } it 'returns 400 when nothing sent' do project_param = {} @@ -1573,7 +1571,7 @@ describe API::Projects do context 'when authenticated as developer' do before do - project_member2 + project_member end it 'returns forbidden error' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 4919ad198339457a4f0259f7199a567bae97db60..a2503dbeb694ff1991b03121577f875a2bc5f471 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -287,7 +287,7 @@ describe API::Users do expect(json_response['message']['projects_limit']). to eq(['must be greater than or equal to 0']) expect(json_response['message']['username']). - to eq([Gitlab::Regex.namespace_regex_message]) + to eq([Gitlab::PathRegex.namespace_format_message]) end it "is not available for non admin users" do @@ -459,7 +459,7 @@ describe API::Users do expect(json_response['message']['projects_limit']). to eq(['must be greater than or equal to 0']) expect(json_response['message']['username']). - to eq([Gitlab::Regex.namespace_regex_message]) + to eq([Gitlab::PathRegex.namespace_format_message]) end it 'returns 400 if provider is missing for identity update' do diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb index c2e8c3ae6f730675b522cd7a672e33663d080cb1..386f60065adfe0d0a29d8498293ce90d31b7d47a 100644 --- a/spec/requests/api/v3/commits_spec.rb +++ b/spec/requests/api/v3/commits_spec.rb @@ -5,7 +5,6 @@ describe API::V3::Commits do let(:user) { create(:user) } let(:user2) { create(:user) } let!(:project) { create(:project, :repository, creator: user, namespace: user.namespace) } - let!(:master) { create(:project_member, :master, user: user, project: project) } let!(:guest) { create(:project_member, :guest, user: user2, project: project) } let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') } diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb index bc261b5e07ca7607ff025bc267439cb6b506393a..98e8c954909d6a29ba35aa8ca874624bbd6bcfca 100644 --- a/spec/requests/api/v3/groups_spec.rb +++ b/spec/requests/api/v3/groups_spec.rb @@ -421,7 +421,7 @@ describe API::V3::Groups do expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled]) end - it "creates a nested group" do + it "creates a nested group", :nested_groups do parent = create(:group) parent.add_owner(user3) group = attributes_for(:group, { parent_id: parent.id }) diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index dc7c3d125b1d15dcd22e4db7036a409d8735d142..bc591b2eb37238a8b4dee40f0af3fae760d541d9 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -10,8 +10,7 @@ describe API::V3::Projects do let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } let(:project2) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace) } let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } - let(:project_member) { create(:project_member, :master, user: user, project: project) } - let(:project_member2) { create(:project_member, :developer, user: user3, project: project) } + let(:project_member) { create(:project_member, :developer, user: user3, project: project) } let(:user4) { create(:user) } let(:project3) do create(:project, @@ -25,7 +24,7 @@ describe API::V3::Projects do issues_enabled: false, wiki_enabled: false, snippets_enabled: false) end - let(:project_member3) do + let(:project_member2) do create(:project_member, user: user4, project: project3, @@ -286,7 +285,7 @@ describe API::V3::Projects do let(:public_project) { create(:empty_project, :public) } before do - project_member2 + project_member user3.update_attributes(starred_projects: [project, project2, project3, public_project]) end @@ -622,7 +621,6 @@ describe API::V3::Projects do context 'when authenticated' do before do project - project_member end it 'returns a project by id' do @@ -814,8 +812,7 @@ describe API::V3::Projects do describe 'GET /projects/:id/users' do shared_examples_for 'project users response' do it 'returns the project users' do - member = create(:user) - create(:project_member, :developer, user: member, project: project) + member = project.owner get v3_api("/projects/#{project.id}/users", current_user) @@ -1163,8 +1160,8 @@ describe API::V3::Projects do before { user4 } before { project3 } before { project4 } - before { project_member3 } before { project_member2 } + before { project_member } context 'when unauthenticated' do it 'returns authentication error' do diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index e5fc0b676af4abcddab35e33e5098459fa2d52a6..179fc9733ad72f0df5d41d613e396d095ff83e9a 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -103,6 +103,18 @@ describe Admin::HooksController, "routing" do end end +# admin_hook_hook_log_retry GET /admin/hooks/:hook_id/hook_logs/:id/retry(.:format) admin/hook_logs#retry +# admin_hook_hook_log GET /admin/hooks/:hook_id/hook_logs/:id(.:format) admin/hook_logs#show +describe Admin::HookLogsController, 'routing' do + it 'to #retry' do + expect(get('/admin/hooks/1/hook_logs/1/retry')).to route_to('admin/hook_logs#retry', hook_id: '1', id: '1') + end + + it 'to #show' do + expect(get('/admin/hooks/1/hook_logs/1')).to route_to('admin/hook_logs#show', hook_id: '1', id: '1') + end +end + # admin_logs GET /admin/logs(.:format) admin/logs#show describe Admin::LogsController, "routing" do it "to #show" do diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index a391c046f92419ae037d2d7d9603e6799502f9ad..54417f6b3e146607223608c9c77fabb1f256eceb 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -349,6 +349,18 @@ describe 'project routing' do end end + # retry_namespace_project_hook_hook_log GET /:project_id/hooks/:hook_id/hook_logs/:id/retry(.:format) projects/hook_logs#retry + # namespace_project_hook_hook_log GET /:project_id/hooks/:hook_id/hook_logs/:id(.:format) projects/hook_logs#show + describe Projects::HookLogsController, 'routing' do + it 'to #retry' do + expect(get('/gitlab/gitlabhq/hooks/1/hook_logs/1/retry')).to route_to('projects/hook_logs#retry', namespace_id: 'gitlab', project_id: 'gitlabhq', hook_id: '1', id: '1') + end + + it 'to #show' do + expect(get('/gitlab/gitlabhq/hooks/1/hook_logs/1')).to route_to('projects/hook_logs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', hook_id: '1', id: '1') + end + end + # project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /\h{7,40}/, project_id: /[^\/]+/} describe Projects::CommitController, 'routing' do it 'to #show' do diff --git a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..968dcd6232ef55f12ce8696b1d4c5a8c85ec902a --- /dev/null +++ b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/migration/update_column_in_batches' + +describe RuboCop::Cop::Migration::UpdateColumnInBatches do + let(:cop) { described_class.new } + let(:tmp_rails_root) { Rails.root.join('tmp', 'rails_root') } + let(:migration_code) do + <<-END + def up + update_column_in_batches(:projects, :name, "foo") do |table, query| + query.where(table[:name].eq(nil)) + end + end + END + end + + before do + allow(cop).to receive(:rails_root).and_return(tmp_rails_root) + end + after do + FileUtils.rm_rf(tmp_rails_root) + end + + context 'outside of a migration' do + it 'does not register any offenses' do + inspect_source(cop, migration_code) + + expect(cop.offenses).to be_empty + end + end + + let(:spec_filepath) { tmp_rails_root.join('spec', 'migrations', 'my_super_migration_spec.rb') } + + shared_context 'with a migration file' do + before do + FileUtils.mkdir_p(File.dirname(migration_filepath)) + @migration_file = File.new(migration_filepath, 'w+') + end + after do + @migration_file.close + end + end + + shared_examples 'a migration file with no spec file' do + include_context 'with a migration file' + + let(:relative_spec_filepath) { Pathname.new(spec_filepath).relative_path_from(tmp_rails_root) } + + it 'registers an offense when using update_column_in_batches' do + inspect_source(cop, migration_code, @migration_file) + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([2]) + expect(cop.offenses.first.message). + to include("`#{relative_spec_filepath}`") + end + end + end + + shared_examples 'a migration file with a spec file' do + include_context 'with a migration file' + + before do + FileUtils.mkdir_p(File.dirname(spec_filepath)) + @spec_file = File.new(spec_filepath, 'w+') + end + after do + @spec_file.close + end + + it 'does not register any offenses' do + inspect_source(cop, migration_code, @migration_file) + + expect(cop.offenses).to be_empty + end + end + + context 'in a migration' do + let(:migration_filepath) { tmp_rails_root.join('db', 'migrate', '20121220064453_my_super_migration.rb') } + + it_behaves_like 'a migration file with no spec file' + it_behaves_like 'a migration file with a spec file' + end + + context 'in a post migration' do + let(:migration_filepath) { tmp_rails_root.join('db', 'post_migrate', '20121220064453_my_super_migration.rb') } + + it_behaves_like 'a migration file with no spec file' + it_behaves_like 'a migration file with a spec file' + end +end diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 0a1f41719f7e4b5187ef7201e7305740ed553ee5..be0e829880e1a28886f2dca3ad66992ef204ceb1 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -41,6 +41,12 @@ describe Issues::CloseService, services: true do service.execute(issue) end + + it 'invalidates counter cache for assignees' do + expect_any_instance_of(User).to receive(:invalidate_issue_cache_counts) + + service.execute(issue) + end end describe '#close_issue' do diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb index 93a8270fd16e477c97aab4842539e19eb76c032b..391ecad303a7e7722f7260c86cd218f76a9ea779 100644 --- a/spec/services/issues/reopen_service_spec.rb +++ b/spec/services/issues/reopen_service_spec.rb @@ -27,6 +27,13 @@ describe Issues::ReopenService, services: true do project.team << [user, :master] end + it 'invalidates counter cache for assignees' do + issue.assignees << user + expect_any_instance_of(User).to receive(:invalidate_issue_cache_counts) + + described_class.new(project, user).execute(issue) + end + context 'when issue is not confidential' do it 'executes issue hooks' do expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks) diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index d55a7657c0ea2acb160ae58aa1c448ee427ce660..154f30aac3b4ab2ed2878eebf885553e9cf9aa15 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -15,6 +15,8 @@ describe MergeRequests::CloseService, services: true do end describe '#execute' do + it_behaves_like 'cache counters invalidator' + context 'valid params' do let(:service) { described_class.new(project, user, {}) } diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a20b32eda5f1aa90992463b1255a33d2293e4af7 --- /dev/null +++ b/spec/services/merge_requests/post_merge_service_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe MergeRequests::PostMergeService, services: true do + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request, assignee: user) } + let(:project) { merge_request.project } + + before do + project.team << [user, :master] + end + + describe '#execute' do + it_behaves_like 'cache counters invalidator' + end +end diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index a99d4eac9bde5db6288496b3c8cc6597218029e0..b6d4db2f922e638e65b49a26966e7e0b5cd799cd 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -14,6 +14,8 @@ describe MergeRequests::ReopenService, services: true do end describe '#execute' do + it_behaves_like 'cache counters invalidator' + context 'valid params' do let(:service) { described_class.new(project, user, {}) } diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 4b8589b2736583b20786d9cf0f29373164b9e426..0d6dd28e33218aedf4c630d0e77e8ed976a60f4d 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -70,7 +70,7 @@ describe Projects::DestroyService, services: true do end end - expect(project.team.members.count).to eq 1 + expect(project.team.members.count).to eq 2 end end diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index 2112f1cf9ea48bfd338065212ee06f428613d128..5cf989105d0129353257922aaa839d52e37a0670 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -26,6 +26,15 @@ describe SearchService, services: true do expect(project).to eq accessible_project end + + it 'returns the project for guests' do + search_project = create :empty_project + search_project.add_guest(user) + + project = SearchService.new(user, project_id: search_project.id).project + + expect(project).to eq search_project + end end context 'when the project is not accessible' do diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb index b19374ef1a2d912928acc1985a53ceebbfe04d37..8c40d25e00c1b3942c35d221e6e637e79affa7bc 100644 --- a/spec/services/users/refresh_authorized_projects_service_spec.rb +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -1,15 +1,13 @@ require 'spec_helper' describe Users::RefreshAuthorizedProjectsService do - let(:project) { create(:empty_project) } + # We're using let! here so that any expectations for the service class are not + # triggered twice. + let!(:project) { create(:empty_project) } + let(:user) { project.namespace.owner } let(:service) { described_class.new(user) } - def create_authorization(project, user, access_level = Gitlab::Access::MASTER) - ProjectAuthorization. - create!(project: project, user: user, access_level: access_level) - end - describe '#execute', :redis do it 'refreshes the authorizations using a lease' do expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain). @@ -31,7 +29,8 @@ describe Users::RefreshAuthorizedProjectsService do it 'updates the authorized projects of the user' do project2 = create(:empty_project) - to_remove = create_authorization(project2, user) + to_remove = user.project_authorizations. + create!(project: project2, access_level: Gitlab::Access::MASTER) expect(service).to receive(:update_authorizations). with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) @@ -40,7 +39,10 @@ describe Users::RefreshAuthorizedProjectsService do end it 'sets the access level of a project to the highest available level' do - to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER) + user.project_authorizations.delete_all + + to_remove = user.project_authorizations. + create!(project: project, access_level: Gitlab::Access::DEVELOPER) expect(service).to receive(:update_authorizations). with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) @@ -61,34 +63,10 @@ describe Users::RefreshAuthorizedProjectsService do service.update_authorizations([], []) end - - context 'when the authorized projects column is not set' do - before do - user.update!(authorized_projects_populated: nil) - end - - it 'populates the authorized projects column' do - service.update_authorizations([], []) - - expect(user.authorized_projects_populated).to eq true - end - end - - context 'when the authorized projects column is set' do - before do - user.update!(authorized_projects_populated: true) - end - - it 'does nothing' do - expect(user).not_to receive(:set_authorized_projects_column) - - service.update_authorizations([], []) - end - end end it 'removes authorizations that should be removed' do - authorization = create_authorization(project, user) + authorization = user.project_authorizations.find_by(project_id: project.id) service.update_authorizations([authorization.project_id]) @@ -96,6 +74,8 @@ describe Users::RefreshAuthorizedProjectsService do end it 'inserts authorizations that should be added' do + user.project_authorizations.delete_all + service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]]) authorizations = user.project_authorizations @@ -105,16 +85,6 @@ describe Users::RefreshAuthorizedProjectsService do expect(authorizations[0].project_id).to eq(project.id) expect(authorizations[0].access_level).to eq(Gitlab::Access::MASTER) end - - it 'populates the authorized projects column' do - # make sure we start with a nil value no matter what the default in the - # factory may be. - user.update!(authorized_projects_populated: nil) - - service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]]) - - expect(user.authorized_projects_populated).to eq(true) - end end describe '#fresh_access_levels_per_project' do @@ -163,7 +133,7 @@ describe Users::RefreshAuthorizedProjectsService do end end - context 'projects of subgroups of groups the user is a member of' do + context 'projects of subgroups of groups the user is a member of', :nested_groups do let(:group) { create(:group) } let(:nested_group) { create(:group, parent: group) } let!(:other_project) { create(:empty_project, group: nested_group) } @@ -191,7 +161,7 @@ describe Users::RefreshAuthorizedProjectsService do end end - context 'projects shared with subgroups of groups the user is a member of' do + context 'projects shared with subgroups of groups the user is a member of', :nested_groups do let(:group) { create(:group) } let(:nested_group) { create(:group, parent: group) } let(:other_project) { create(:empty_project) } @@ -208,8 +178,6 @@ describe Users::RefreshAuthorizedProjectsService do end describe '#current_authorizations_per_project' do - before { create_authorization(project, user) } - let(:hash) { service.current_authorizations_per_project } it 'returns a Hash' do @@ -233,13 +201,13 @@ describe Users::RefreshAuthorizedProjectsService do describe '#current_authorizations' do context 'without authorizations' do it 'returns an empty list' do + user.project_authorizations.delete_all + expect(service.current_authorizations.empty?).to eq(true) end end context 'with an authorization' do - before { create_authorization(project, user) } - let(:row) { service.current_authorizations.take } it 'returns the currently authorized projects' do diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b5abc46e80c348f1e11b5dcc65a0f453ffdba38d --- /dev/null +++ b/spec/services/web_hook_service_spec.rb @@ -0,0 +1,137 @@ +require 'spec_helper' + +describe WebHookService, services: true do + let(:project) { create(:empty_project) } + let(:project_hook) { create(:project_hook) } + let(:headers) do + { + 'Content-Type' => 'application/json', + 'X-Gitlab-Event' => 'Push Hook' + } + end + let(:data) do + { before: 'oldrev', after: 'newrev', ref: 'ref' } + end + let(:service_instance) { WebHookService.new(project_hook, data, 'push_hooks') } + + describe '#execute' do + before(:each) do + project.hooks << [project_hook] + + WebMock.stub_request(:post, project_hook.url) + end + + context 'when token is defined' do + let(:project_hook) { create(:project_hook, :token) } + + it 'POSTs to the webhook URL' do + service_instance.execute + expect(WebMock).to have_requested(:post, project_hook.url).with( + headers: headers.merge({ 'X-Gitlab-Token' => project_hook.token }) + ).once + end + end + + it 'POSTs to the webhook URL' do + service_instance.execute + expect(WebMock).to have_requested(:post, project_hook.url).with( + headers: headers + ).once + end + + it 'POSTs the data as JSON' do + service_instance.execute + expect(WebMock).to have_requested(:post, project_hook.url).with( + headers: headers + ).once + end + + it 'catches exceptions' do + WebMock.stub_request(:post, project_hook.url).to_raise(StandardError.new('Some error')) + + expect { service_instance.execute }.to raise_error(StandardError) + end + + it 'handles exceptions' do + exceptions = [SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout] + exceptions.each do |exception_class| + exception = exception_class.new('Exception message') + + WebMock.stub_request(:post, project_hook.url).to_raise(exception) + expect(service_instance.execute).to eq([nil, exception.message]) + expect { service_instance.execute }.not_to raise_error + end + end + + it 'handles 200 status code' do + WebMock.stub_request(:post, project_hook.url).to_return(status: 200, body: 'Success') + + expect(service_instance.execute).to eq([200, 'Success']) + end + + it 'handles 2xx status codes' do + WebMock.stub_request(:post, project_hook.url).to_return(status: 201, body: 'Success') + + expect(service_instance.execute).to eq([201, 'Success']) + end + + context 'execution logging' do + let(:hook_log) { project_hook.web_hook_logs.last } + + context 'with success' do + before do + WebMock.stub_request(:post, project_hook.url).to_return(status: 200, body: 'Success') + service_instance.execute + end + + it 'log successful execution' do + expect(hook_log.trigger).to eq('push_hooks') + expect(hook_log.url).to eq(project_hook.url) + expect(hook_log.request_headers).to eq(headers) + expect(hook_log.response_body).to eq('Success') + expect(hook_log.response_status).to eq('200') + expect(hook_log.execution_duration).to be > 0 + expect(hook_log.internal_error_message).to be_nil + end + end + + context 'with exception' do + before do + WebMock.stub_request(:post, project_hook.url).to_raise(SocketError.new('Some HTTP Post error')) + service_instance.execute + end + + it 'log failed execution' do + expect(hook_log.trigger).to eq('push_hooks') + expect(hook_log.url).to eq(project_hook.url) + expect(hook_log.request_headers).to eq(headers) + expect(hook_log.response_body).to eq('') + expect(hook_log.response_status).to eq('internal error') + expect(hook_log.execution_duration).to be > 0 + expect(hook_log.internal_error_message).to eq('Some HTTP Post error') + end + end + + context 'should not log ServiceHooks' do + let(:service_hook) { create(:service_hook) } + let(:service_instance) { WebHookService.new(service_hook, data, 'service_hook') } + + before do + WebMock.stub_request(:post, service_hook.url).to_return(status: 200, body: 'Success') + end + + it { expect { service_instance.execute }.not_to change(WebHookLog, :count) } + end + end + end + + describe '#async_execute' do + let(:system_hook) { create(:system_hook) } + + it 'enqueue WebHookWorker' do + expect(Sidekiq::Client).to receive(:enqueue).with(WebHookWorker, project_hook.id, data, 'push_hooks') + + WebHookService.new(project_hook, data, 'push_hooks').async_execute + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 51571ddebe98fa202c99a5c635f264845c600b03..4c2eba8fa46812f92662db977101078bc99667c9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -92,6 +92,14 @@ RSpec.configure do |config| Gitlab::Redis.with(&:flushall) Sidekiq.redis(&:flushall) end + + config.around(:each, :nested_groups) do |example| + example.run if Group.supports_nested_groups? + end + + config.around(:each, :postgresql) do |example| + example.run if Gitlab::Database.postgresql? + end end FactoryGirl::SyntaxRunner.class_eval do diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb index c59b30c772de08ac3a5a490d47b50aabbde73360..d6b40db09ce3b05e7aae92a4a1703fa45d72703a 100644 --- a/spec/support/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb @@ -209,9 +209,13 @@ shared_examples 'a GitHub-ish import controller: POST create' do end context 'user has chosen a namespace and name for the project' do - let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) } + let(:test_namespace) { create(:group, name: 'test_namespace') } let(:test_name) { 'test_name' } + before do + test_namespace.add_owner(user) + end + it 'takes the selected namespace and name' do expect(Gitlab::GithubImport::ProjectCreator). to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider). @@ -230,10 +234,14 @@ shared_examples 'a GitHub-ish import controller: POST create' do end context 'user has chosen an existing nested namespace and name for the project' do - let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) } - let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) } + let(:parent_namespace) { create(:group, name: 'foo', owner: user) } + let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) } let(:test_name) { 'test_name' } + before do + nested_namespace.add_owner(user) + end + it 'takes the selected namespace and name' do expect(Gitlab::GithubImport::ProjectCreator). to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider). @@ -276,7 +284,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do context 'user has chosen existent and non-existent nested namespaces and name for the project' do let(:test_name) { 'test_name' } - let!(:parent_namespace) { create(:namespace, name: 'foo', owner: user) } + let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } it 'takes the selected namespace and name' do expect(Gitlab::GithubImport::ProjectCreator). diff --git a/spec/support/issuable_shared_examples.rb b/spec/support/issuable_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..03011535351e0c1c56b11080eb20ed6afa613e78 --- /dev/null +++ b/spec/support/issuable_shared_examples.rb @@ -0,0 +1,7 @@ +shared_examples 'cache counters invalidator' do + it 'invalidates counter cache for assignees' do + expect_any_instance_of(User).to receive(:invalidate_merge_request_cache_counts) + + described_class.new(project, user, {}).execute(merge_request) + end +end diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb index d2a1ded57ff2e1d76e290b179958166c9a38ee6c..9280fad4ace2ff4062152c16dfe81f8a8273ac32 100644 --- a/spec/support/kubernetes_helpers.rb +++ b/spec/support/kubernetes_helpers.rb @@ -41,7 +41,7 @@ module KubernetesHelpers containers.map do |container| terminal = { selectors: { pod: pod_name, container: container['name'] }, - url: container_exec_url(service.api_url, service.namespace, pod_name, container['name']), + url: container_exec_url(service.api_url, service.actual_namespace, pod_name, container['name']), subprotocols: ['channel.k8s.io'], headers: { 'Authorization' => ["Bearer #{service.token}"] }, created_at: DateTime.parse(pod['metadata']['creationTimestamp']), diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index b168098edea495b727e2e856d2cbc89b8f61d0ad..3c000feba5df25b96cc27557e6a74abb301bdffa 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -123,7 +123,7 @@ module TestEnv socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '') gitaly_dir = File.dirname(socket_path) - unless File.directory?(gitaly_dir) || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]") + unless !gitaly_needs_update?(gitaly_dir) || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]") raise "Can't clone gitaly" end @@ -252,4 +252,15 @@ module TestEnv cleanup && init unless reset.call end end + + def gitaly_needs_update?(gitaly_dir) + gitaly_version = File.read(File.join(gitaly_dir, 'VERSION')).strip + + # Notice that this will always yield true when using branch versions + # (`=branch_name`), but that actually makes sure the server is always based + # on the latest branch revision. + gitaly_version != Gitlab::GitalyClient.expected_server_version + rescue Errno::ENOENT + true + end end diff --git a/spec/validators/dynamic_path_validator_spec.rb b/spec/validators/dynamic_path_validator_spec.rb index 03e23781d1bd4da9d56afd6e36e9e29f34e6b69e..5f998e78f075b0135feb2db31c62150d45afb4bb 100644 --- a/spec/validators/dynamic_path_validator_spec.rb +++ b/spec/validators/dynamic_path_validator_spec.rb @@ -15,31 +15,31 @@ describe DynamicPathValidator do end context 'for group' do - it 'calls valid_namespace_path?' do + it 'calls valid_group_path?' do group = build(:group, :nested, path: 'activity') - expect(described_class).to receive(:valid_namespace_path?).with(group.full_path).and_call_original + expect(described_class).to receive(:valid_group_path?).with(group.full_path).and_call_original expect(validator.path_valid_for_record?(group, 'activity')).to be_falsey end end context 'for user' do - it 'calls valid_namespace_path?' do + it 'calls valid_user_path?' do user = build(:user, username: 'activity') - expect(described_class).to receive(:valid_namespace_path?).with(user.full_path).and_call_original + expect(described_class).to receive(:valid_user_path?).with(user.full_path).and_call_original expect(validator.path_valid_for_record?(user, 'activity')).to be_truthy end end context 'for user namespace' do - it 'calls valid_namespace_path?' do + it 'calls valid_user_path?' do user = create(:user, username: 'activity') namespace = user.namespace - expect(described_class).to receive(:valid_namespace_path?).with(namespace.full_path).and_call_original + expect(described_class).to receive(:valid_user_path?).with(namespace.full_path).and_call_original expect(validator.path_valid_for_record?(namespace, 'activity')).to be_truthy end @@ -52,7 +52,7 @@ describe DynamicPathValidator do validator.validate_each(group, :path, "Path with spaces, and comma's!") - expect(group.errors[:path]).to include(Gitlab::Regex.namespace_regex_message) + expect(group.errors[:path]).to include(Gitlab::PathRegex.namespace_format_message) end it 'adds a message when the path is not in the correct format' do diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb index 6295856b46123a01ae01377daa5736fb484a7f87..4e036285e8c1afb60d9c2f57a6dd32e850dd238a 100644 --- a/spec/workers/process_commit_worker_spec.rb +++ b/spec/workers/process_commit_worker_spec.rb @@ -20,14 +20,6 @@ describe ProcessCommitWorker do worker.perform(project.id, -1, commit.to_hash) end - it 'does not process the commit when no issues are referenced' do - allow(worker).to receive(:build_commit).and_return(double(matches_cross_reference_regex?: false)) - - expect(worker).not_to receive(:process_commit_message) - - worker.perform(project.id, user.id, commit.to_hash) - end - it 'processes the commit message' do expect(worker).to receive(:process_commit_message).and_call_original @@ -39,6 +31,18 @@ describe ProcessCommitWorker do worker.perform(project.id, user.id, commit.to_hash) end + + context 'when commit already exists in upstream project' do + let(:forked) { create(:project, :public) } + + it 'does not process commit message' do + create(:forked_project_link, forked_to_project: forked, forked_from_project: project) + + expect(worker).not_to receive(:process_commit_message) + + worker.perform(forked.id, user.id, forked.commit.to_hash) + end + end end describe '#process_commit_message' do diff --git a/spec/workers/remove_old_web_hook_logs_worker_spec.rb b/spec/workers/remove_old_web_hook_logs_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6d26ba5dfa0b3de118221917970dea0334f3e9e3 --- /dev/null +++ b/spec/workers/remove_old_web_hook_logs_worker_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe RemoveOldWebHookLogsWorker do + subject { described_class.new } + + describe '#perform' do + let!(:week_old_record) { create(:web_hook_log, created_at: Time.now - 1.week) } + let!(:three_days_old_record) { create(:web_hook_log, created_at: Time.now - 3.days) } + let!(:one_day_old_record) { create(:web_hook_log, created_at: Time.now - 1.day) } + + it 'removes web hook logs older than 2 days' do + subject.perform + + expect(WebHookLog.all).to include(one_day_old_record) + expect(WebHookLog.all).not_to include(week_old_record, three_days_old_record) + end + end +end diff --git a/yarn.lock b/yarn.lock index 8aac2b1b1cd72004c07a39d9dcbee72b1b070109..8221711960df0ad3317c6129f0ac41994c7f2b5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,7 +25,7 @@ acorn-jsx@^3.0.0: dependencies: acorn "^3.0.4" -acorn@4.0.4: +acorn@4.0.4, acorn@^4.0.3: version "4.0.4" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a" @@ -33,9 +33,9 @@ acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" -acorn@^4.0.11, acorn@^4.0.3, acorn@^4.0.4: - version "4.0.11" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0" +acorn@^5.0.0, acorn@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.0.3.tgz#c460df08491463f028ccb82eab3730bf01087b3d" after@0.8.2: version "0.8.2" @@ -1560,6 +1560,12 @@ debug@2.6.0, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0: dependencies: ms "0.7.2" +debug@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" + dependencies: + ms "2.0.0" + decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -1612,7 +1618,7 @@ delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" -depd@~1.1.0: +depd@1.1.0, depd@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" @@ -1733,7 +1739,7 @@ ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" -ejs@^2.5.5: +ejs@^2.5.6: version "2.5.6" resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.6.tgz#479636bfa3fe3b1debd52087f0acb204b4f19c88" @@ -2081,9 +2087,9 @@ esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" -etag@~1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" +etag@~1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" eve-raphael@0.5.0: version "0.5.0" @@ -2166,9 +2172,9 @@ exports-loader@^0.6.4: loader-utils "^1.0.2" source-map "0.5.x" -express@^4.13.3, express@^4.14.1: - version "4.14.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.14.1.tgz#646c237f766f148c2120aff073817b9e4d7e0d33" +express@^4.13.3, express@^4.15.2: + version "4.15.3" + resolved "https://registry.yarnpkg.com/express/-/express-4.15.3.tgz#bab65d0f03aa80c358408972fc700f916944b662" dependencies: accepts "~1.3.3" array-flatten "1.1.1" @@ -2176,26 +2182,28 @@ express@^4.13.3, express@^4.14.1: content-type "~1.0.2" cookie "0.3.1" cookie-signature "1.0.6" - debug "~2.2.0" + debug "2.6.7" depd "~1.1.0" encodeurl "~1.0.1" escape-html "~1.0.3" - etag "~1.7.0" - finalhandler "0.5.1" - fresh "0.3.0" + etag "~1.8.0" + finalhandler "~1.0.3" + fresh "0.5.0" merge-descriptors "1.0.1" methods "~1.1.2" on-finished "~2.3.0" parseurl "~1.3.1" path-to-regexp "0.1.7" - proxy-addr "~1.1.3" - qs "6.2.0" + proxy-addr "~1.1.4" + qs "6.4.0" range-parser "~1.2.0" - send "0.14.2" - serve-static "~1.11.2" - type-is "~1.6.14" + send "0.15.3" + serve-static "1.12.3" + setprototypeof "1.0.3" + statuses "~1.3.1" + type-is "~1.6.15" utils-merge "1.0.0" - vary "~1.1.0" + vary "~1.1.1" extend@^3.0.0, extend@~3.0.0: version "3.0.0" @@ -2287,9 +2295,9 @@ filesize@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.3.0.tgz#53149ea3460e3b2e024962a51648aa572cf98122" -filesize@^3.5.4: - version "3.5.4" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.4.tgz#742fc7fb6aef4ee3878682600c22f840731e1fda" +filesize@^3.5.9: + version "3.5.10" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.10.tgz#fc8fa23ddb4ef9e5e0ab6e1e64f679a24a56761f" fill-range@^2.1.0: version "2.2.3" @@ -2311,13 +2319,15 @@ finalhandler@0.5.0: statuses "~1.3.0" unpipe "~1.0.0" -finalhandler@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.1.tgz#2c400d8d4530935bc232549c5fa385ec07de6fcd" +finalhandler@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89" dependencies: - debug "~2.2.0" + debug "2.6.7" + encodeurl "~1.0.1" escape-html "~1.0.3" on-finished "~2.3.0" + parseurl "~1.3.1" statuses "~1.3.1" unpipe "~1.0.0" @@ -2385,9 +2395,9 @@ forwarded@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" -fresh@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" +fresh@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" from@~0: version "0.1.7" @@ -2689,6 +2699,15 @@ http-errors@~1.5.0, http-errors@~1.5.1: setprototypeof "1.0.2" statuses ">= 1.3.1 < 2" +http-errors@~1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" + dependencies: + depd "1.1.0" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + http-proxy-middleware@~0.17.4: version "0.17.4" resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833" @@ -2808,9 +2827,9 @@ invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" -ipaddr.js@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.2.0.tgz#8aba49c9192799585bdd643e0ccb50e8ae777ba4" +ipaddr.js@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec" is-absolute-url@^2.0.0: version "2.1.0" @@ -3198,7 +3217,7 @@ json3@3.3.2, json3@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" -json5@^0.5.0: +json5@^0.5.0, json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" @@ -3643,7 +3662,17 @@ miller-rabin@^4.0.0: version "1.26.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff" -mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7: +mime-db@~1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" + +mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.7: + version "2.1.15" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" + dependencies: + mime-db "~1.27.0" + +mime-types@~2.1.11: version "2.1.14" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee" dependencies: @@ -3699,10 +3728,18 @@ ms@0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + mute-stream@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" +name-all-modules-plugin@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/name-all-modules-plugin/-/name-all-modules-plugin-1.0.1.tgz#0abfb6ad835718b9fb4def0674e06657a954375c" + nan@^2.0.0, nan@^2.3.0: version "2.5.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2" @@ -3931,7 +3968,7 @@ onetime@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" -opener@^1.4.2: +opener@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8" @@ -4485,12 +4522,12 @@ proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" -proxy-addr@~1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.3.tgz#dc97502f5722e888467b3fa2297a7b1ff47df074" +proxy-addr@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3" dependencies: forwarded "~0.1.0" - ipaddr.js "1.2.0" + ipaddr.js "1.3.0" prr@~0.0.0: version "0.0.0" @@ -4532,14 +4569,14 @@ qjobs@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73" -qs@6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b" - qs@6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625" +qs@6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + qs@~6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442" @@ -4913,7 +4950,7 @@ rx-lite@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" -safe-buffer@^5.0.1: +safe-buffer@^5.0.1, safe-buffer@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" @@ -4947,20 +4984,20 @@ semver@~4.3.3: version "4.3.6" resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" -send@0.14.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/send/-/send-0.14.2.tgz#39b0438b3f510be5dc6f667a11f71689368cdeef" +send@0.15.3: + version "0.15.3" + resolved "https://registry.yarnpkg.com/send/-/send-0.15.3.tgz#5013f9f99023df50d1bd9892c19e3defd1d53309" dependencies: - debug "~2.2.0" + debug "2.6.7" depd "~1.1.0" destroy "~1.0.4" encodeurl "~1.0.1" escape-html "~1.0.3" - etag "~1.7.0" - fresh "0.3.0" - http-errors "~1.5.1" + etag "~1.8.0" + fresh "0.5.0" + http-errors "~1.6.1" mime "1.3.4" - ms "0.7.2" + ms "2.0.0" on-finished "~2.3.0" range-parser "~1.2.0" statuses "~1.3.1" @@ -4977,14 +5014,14 @@ serve-index@^1.7.2: mime-types "~2.1.11" parseurl "~1.3.1" -serve-static@~1.11.2: - version "1.11.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.2.tgz#2cf9889bd4435a320cc36895c9aa57bd662e6ac7" +serve-static@1.12.3: + version "1.12.3" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.3.tgz#9f4ba19e2f3030c547f8af99107838ec38d5b1e2" dependencies: encodeurl "~1.0.1" escape-html "~1.0.3" parseurl "~1.3.1" - send "0.14.2" + send "0.15.3" set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" @@ -5002,6 +5039,10 @@ setprototypeof@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + sha.js@^2.3.6: version "2.4.8" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f" @@ -5496,20 +5537,20 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-is@~1.6.14: - version "1.6.14" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2" +type-is@~1.6.14, type-is@~1.6.15: + version "1.6.15" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" dependencies: media-typer "0.3.0" - mime-types "~2.1.13" + mime-types "~2.1.15" typedarray@^0.0.6, typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -uglify-js@^2.6, uglify-js@^2.8.5: - version "2.8.21" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.21.tgz#1733f669ae6f82fc90c7b25ec0f5c783ee375314" +uglify-js@^2.6, uglify-js@^2.8.27: + version "2.8.27" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.27.tgz#47787f912b0f242e5b984343be8e35e95f694c9c" dependencies: source-map "~0.5.1" yargs "~3.10.0" @@ -5528,6 +5569,10 @@ ultron@1.0.x: version "1.0.2" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" +ultron@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864" + unc-path-regex@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" @@ -5640,9 +5685,9 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" -vary@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140" +vary@~1.1.0, vary@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" vendors@^1.0.0: version "1.0.1" @@ -5729,20 +5774,21 @@ wbuf@^1.1.0, wbuf@^1.4.0: dependencies: minimalistic-assert "^1.0.0" -webpack-bundle-analyzer@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.3.0.tgz#0d05e96a43033f7cc57f6855b725782ba61e93a4" +webpack-bundle-analyzer@^2.8.2: + version "2.8.2" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.8.2.tgz#8b6240c29a9d63bc72f09d920fb050adbcce9fe8" dependencies: - acorn "^4.0.11" + acorn "^5.0.3" chalk "^1.1.3" commander "^2.9.0" - ejs "^2.5.5" - express "^4.14.1" - filesize "^3.5.4" + ejs "^2.5.6" + express "^4.15.2" + filesize "^3.5.9" gzip-size "^3.0.0" lodash "^4.17.4" mkdirp "^0.5.1" - opener "^1.4.2" + opener "^1.4.3" + ws "^2.3.1" webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.9.0: version "1.10.0" @@ -5789,11 +5835,11 @@ webpack-sources@^0.2.3: source-list-map "^1.1.1" source-map "~0.5.3" -webpack@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.3.3.tgz#eecc083c18fb7bf958ea4f40b57a6640c5a0cc78" +webpack@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.6.1.tgz#2e0457f0abb1ac5df3ab106c69c672f236785f07" dependencies: - acorn "^4.0.4" + acorn "^5.0.0" acorn-dynamic-import "^2.0.0" ajv "^4.7.0" ajv-keywords "^1.1.1" @@ -5801,6 +5847,7 @@ webpack@^2.3.3: enhanced-resolve "^3.0.0" interpret "^1.0.0" json-loader "^0.5.4" + json5 "^0.5.1" loader-runner "^2.3.0" loader-utils "^0.2.16" memory-fs "~0.4.1" @@ -5809,7 +5856,7 @@ webpack@^2.3.3: source-map "^0.5.3" supports-color "^3.1.0" tapable "~0.2.5" - uglify-js "^2.8.5" + uglify-js "^2.8.27" watchpack "^1.3.1" webpack-sources "^0.2.3" yargs "^6.0.0" @@ -5898,6 +5945,13 @@ ws@1.1.1: options ">=0.0.5" ultron "1.0.x" +ws@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-2.3.1.tgz#6b94b3e447cb6a363f785eaf94af6359e8e81c80" + dependencies: + safe-buffer "~5.0.1" + ultron "~1.1.0" + wtf-8@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wtf-8/-/wtf-8-1.0.0.tgz#392d8ba2d0f1c34d1ee2d630f15d0efb68e1048a"