diff options
171 files changed, 5340 insertions, 1054 deletions
diff --git a/CHANGELOG b/CHANGELOG index ff5996c1174..491692204a6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ v 8.10.0 (unreleased) - Wrap code blocks on Activies and Todos page. !4783 (winniehell) - Align flash messages with left side of page content !4959 (winniehell) - Display last commit of deleted branch in push events !4699 (winniehell) + - Escape file extension when parsing search results !5141 (winniehell) - Apply the trusted_proxies config to the rack request object for use with rack_attack - Add Sidekiq queue duration to transaction metrics. - Add a new column `artifacts_size` to table `ci_builds` !4964 @@ -15,10 +16,13 @@ v 8.10.0 (unreleased) - Make images fit to the size of the viewport !4810 - Fix check for New Branch button on Issue page !4630 (winniehell) - Fix MR-auto-close text added to description. !4836 + - Fix issue, preventing users w/o push access to sort tags !5105 (redetection) - Add Spring EmojiOne updates. + - Fix viewing notification settings when a project is pending deletion - Fix pagination when sorting by columns with lots of ties (like priority) - Updated project header design - Exclude email check from the standard health check + - Updated layout for Projects, Groups, Users on Admin area !4424 - Fix changing issue state columns in milestone view - Add notification settings dropdown for groups - Allow importing from Github using Personal Access Tokens. (Eric K Idema) @@ -28,6 +32,7 @@ v 8.10.0 (unreleased) - PipelinesFinder uses git cache data - Throttle the update of `project.pushes_since_gc` to 1 minute. - Check for conflicts with existing Project's wiki path when creating a new project. + - Show last push widget in upstream after push to fork - Don't instantiate a git tree on Projects show default view - Bump Rinku to 2.0.0 - Remove unused front-end variable -> default_issues_tracker @@ -44,17 +49,24 @@ v 8.10.0 (unreleased) - More descriptive message for git hooks and file locks - Handle custom Git hook result in GitLab UI - Allow '?', or '&' for label names + - Fix importer for GitHub Pull Requests when a branch was reused across Pull Requests + - Add date when user joined the team on the member page + - Fix 404 redirect after validation fails importing a GitLab project -v 8.9.5 (unreleased) - - Improve the request / withdraw access button. !4860 - - Fix assigning shared runners as admins. !4961 - - Show "locked" label for locked runners on runners admin. !4961 - - Downgrade to Redis 3.2.2 due to massive memory leak with Sidekiq - - Add index on the user and emoji name on AwardEmoji table !5061 - - Fixes issues importing events in Import/Export. Import/Export version bumped to 0.1.1 - - Fix import button disabled when import process fail due to the namespace already been taken. +v 8.9.5 + - Add more debug info to import/export and memory killer. !5108 + - Fixed avatar alignment in new MR view. !5095 - Fix diff comments not showing up in activity feed. !5069 - - Security: Update RedCloth to 4.3.2 (Takuya Noguchi) + - Add index on both Award Emoji user and name. !5061 + - Downgrade to Redis 3.2.2 due to massive memory leak with Sidekiq. !5056 + - Re-enable import button when import process fails due to namespace already being taken. !5053 + - Fix snippets comments not displayed. !5045 + - Fix emoji paths in relative root configurations. !5027 + - Fix issues importing events in Import/Export. !4987 + - Fixed 'use shortcuts' button on docs. !4979 + - Admin should be able to turn shared runners into specific ones. !4961 + - Update RedCloth to 4.3.2 for CVE-2012-6684. !4929 (Takuya Noguchi) + - Improve the request / withdraw access button. !4860 v 8.9.4 - Fix privilege escalation issue with OAuth external users. diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee index 2d515d7efa2..74691b2c1b5 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/ci/build.coffee @@ -2,7 +2,7 @@ class @CiBuild @interval: null @state: null - constructor: (@build_url, @build_status, @state) -> + constructor: (@page_url, @build_url, @build_status, @state) -> clearInterval(CiBuild.interval) # Init breakpoint checker @@ -41,7 +41,7 @@ class @CiBuild # Only valid for runnig build when output changes during time # CiBuild.interval = setInterval => - if window.location.href.split("#").first() is @build_url + if window.location.href.split("#").first() is @page_url @getBuildTrace() , 4000 @@ -57,7 +57,7 @@ class @CiBuild getBuildTrace: -> $.ajax - url: "#{@build_url}/trace.json?state=#{encodeURIComponent(@state)}" + url: "#{@page_url}/trace.json?state=#{encodeURIComponent(@state)}" dataType: "json" success: (log) => if log.state @@ -70,7 +70,7 @@ class @CiBuild $('.js-build-output').html log.html @checkAutoscroll() else if log.status isnt @build_status - Turbolinks.visit @build_url + Turbolinks.visit @page_url checkAutoscroll: -> $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state") diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 9493a575801..a39df421832 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -127,7 +127,7 @@ class Dispatcher when 'groups' new UsersSelect() when 'projects' - new NamespaceSelect() + new NamespaceSelects() when 'dashboard', 'root' shortcut_handler = new ShortcutsDashboardNavigation() when 'profiles' diff --git a/app/assets/javascripts/namespace_select.js.coffee b/app/assets/javascripts/namespace_select.js.coffee index a02c4515ccc..3b419dff105 100644 --- a/app/assets/javascripts/namespace_select.js.coffee +++ b/app/assets/javascripts/namespace_select.js.coffee @@ -1,25 +1,56 @@ class @NamespaceSelect - constructor: -> - namespaceFormatResult = (namespace) -> - markup = "<div class='namespace-result'>" - markup += "<span class='namespace-kind'>" + namespace.kind + "</span>" - markup += "<span class='namespace-path'>" + namespace.path + "</span>" - markup += "</div>" - markup - - formatSelection = (namespace) -> - namespace.kind + ": " + namespace.path - - $('.ajax-namespace-select').each (i, select) -> - $(select).select2 - placeholder: "Search for namespace" - multiple: $(select).hasClass('multiselect') - minimumInputLength: 0 - query: (query) -> - Api.namespaces query.term, (namespaces) -> - data = { results: namespaces } - query.callback(data) - - dropdownCssClass: "ajax-namespace-dropdown" - formatResult: namespaceFormatResult - formatSelection: formatSelection + constructor: (opts) -> + { + @dropdown + } = opts + + showAny = true + fieldName = 'namespace_id' + + if @dropdown.attr 'data-field-name' + fieldName = @dropdown.data 'fieldName' + + if @dropdown.attr 'data-show-any' + showAny = @dropdown.data 'showAny' + + @dropdown.glDropdown( + filterable: true + selectable: true + filterRemote: true + search: + fields: ['path'] + fieldName: fieldName + toggleLabel: (selected) -> + return if not selected.id? then selected.text else "#{selected.kind}: #{selected.path}" + data: (term, dataCallback) -> + Api.namespaces term, (namespaces) -> + if showAny + anyNamespace = + text: 'Any namespace' + id: null + + namespaces.unshift(anyNamespace) + namespaces.splice 1, 0, 'divider' + + dataCallback(namespaces) + text: (namespace) -> + return if not namespace.id? then namespace.text else "#{namespace.kind}: #{namespace.path}" + renderRow: @renderRow + clicked: @onSelectItem + ) + + onSelectItem: (item, el, e) => + e.preventDefault() + +class @NamespaceSelects + constructor: (opts = {}) -> + { + @$dropdowns = $('.js-namespace-select') + } = opts + + @$dropdowns.each (i, dropdown) -> + $dropdown = $(dropdown) + + new NamespaceSelect( + dropdown: $dropdown + ) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 7c1d943667b..0b7d8f64456 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -240,12 +240,16 @@ class @Notes @note_ids.push(note.id) form = $("#new-discussion-note-form-#{note.discussion_id}") + if note.original_discussion_id? and form.length is 0 + form = $("#new-discussion-note-form-#{note.original_discussion_id}") row = form.closest("tr") note_html = $(note.html) note_html.syntaxHighlight() # is this the first note of discussion? discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']") + if note.original_discussion_id? and discussionContainer.length is 0 + discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']") if discussionContainer.length is 0 # insert the note and the reply button after the temp row row.after note.discussion_html @@ -318,6 +322,7 @@ class @Notes form.addClass "js-main-target-form" form.find("#note_line_code").remove() + form.find("#note_position").remove() form.find("#note_type").remove() ### @@ -335,10 +340,12 @@ class @Notes new Autosave textarea, [ "Note" - form.find("#note_commit_id").val() - form.find("#note_line_code").val() 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("#note_line_code").val() + form.find("#note_position").val() ] ### @@ -512,10 +519,12 @@ class @Notes setupDiscussionNoteForm: (dataHolder, form) => # setup note target form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}" + form.attr "data-line-code", dataHolder.data("lineCode") form.find("#note_type").val dataHolder.data("noteType") form.find("#line_type").val dataHolder.data("lineType") form.find("#note_commit_id").val dataHolder.data("commitId") form.find("#note_line_code").val dataHolder.data("lineCode") + form.find("#note_position").val dataHolder.attr("data-position") form.find("#note_noteable_type").val dataHolder.data("noteableType") form.find("#note_noteable_id").val dataHolder.data("noteableId") form.find('.js-note-discard') diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index 40b5171a8c6..d28cda6d62d 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -1,3 +1,12 @@ +.blank-state-welcome { + text-align: center; + border-bottom: 1px solid $border-color; + + .blank-state-text { + margin-bottom: 0; + } +} + .blank-state { padding-top: 20px; padding-bottom: 20px; @@ -6,7 +15,15 @@ .blank-state-no-icon { padding-top: 40px; - padding-bottom: 40px; + padding-bottom: 40px; +} + +.blank-state-icon { + padding-bottom: 20px; + + path { + fill: $gray-darkest; + } } .blank-state-title { @@ -21,3 +38,7 @@ margin-bottom: $gl-padding; font-size: 15px; } + +.blank-state-welcome-title { + font-size: 24px; +} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 2a90a1fef37..d4e900f80ef 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -23,6 +23,9 @@ .dropdown-menu, .dropdown-menu-nav { display: block; + @media (max-width: $screen-xs-max) { + width: 100%; + } } .dropdown-menu-toggle { @@ -65,6 +68,10 @@ color: $dropdown-toggle-hover-icon-color; } } + + &.large { + width: 200px; + } } .dropdown-menu, diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 71a9f79be3e..71e4b50f2af 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -50,7 +50,7 @@ } a:not(.btn) { - color: $gl-dark-link-color; + color: $gl-text-color; } .left-options { diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index a12c0bba44a..aed0b44d91b 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -137,6 +137,15 @@ ul.content-list { padding-top: 1px; float: right; + > .control-text { + margin-right: $gl-padding-top; + line-height: 40px; + + &:last-child { + margin-right: 0; + } + } + > .btn, > .btn-group { margin-right: $gl-padding-top; diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 6e5f216c894..02ea98e9d94 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -134,6 +134,11 @@ margin-bottom: 0; border-bottom: none; + &.wide { + width: 100%; + display: block; + } + li a { padding: 16px 10px 11px; } @@ -164,6 +169,7 @@ > .btn { margin-right: $gl-padding-top; display: inline-block; + vertical-align: top; &:last-child { margin-right: 0; diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index e05f14e7496..1d34a7f79ae 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -71,3 +71,36 @@ @extend .broadcast-message; margin-bottom: 20px; } + + +// Users List + +.users-list { + .user-row { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + } + + .user-details { + flex: 1 1 auto; + } + + .user-name { + display: inline-block; + font-weight: bold; + } + + .controls { + > .btn, > .dropdown { + margin-left: 5px; + } + } + + .dropdown { + .btn-block { + margin-bottom: 0; + line-height: inherit; + } + } +} diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 5286b73cc50..21b1c223c88 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -434,13 +434,3 @@ } } } - -.discussion { - .diff-content { - .diff-line-num { - &:before { - content: attr(data-linenumber); - } - } - } -} diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 3d79f4400e2..701b9388454 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -38,6 +38,39 @@ margin-right: 15px; } } + + &.group-admin { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + + .group-avatar, .group-details, .group-controls { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + } + + .group-details { + flex: 1 1 auto; + flex-direction: column; + min-width: 0; + } + + .group-controls { + align-items: center; + + a { + margin-left: 5px; + } + } + } + +} + +.ldap-group-links { + .form-actions { + margin-bottom: $gl-padding; + } } .groups-cover-block { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 3325b586496..bce4aac3334 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -475,10 +475,6 @@ pre.light-well { a:hover { text-decoration: none; } - - > span { - margin-left: 10px; - } } } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index ae524cd6bae..9e9b18fdbb8 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -208,7 +208,7 @@ margin-top: 5px; @media (min-width: $screen-sm-min) { - width: 160px; + width: 180px; margin-top: 0; } } diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 4c9c6362ffc..0d2f4f6eb38 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -5,11 +5,12 @@ class Admin::ProjectsController < Admin::ApplicationController def index @projects = Project.all @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? - @projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? + @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? @projects = @projects.where(last_repository_check_failed: true) if params[:last_repository_check_failed].present? - @projects = @projects.non_archived unless params[:with_archived].present? + @projects = @projects.non_archived unless params[:archived].present? + @projects = @projects.personal(current_user) if params[:personal].present? @projects = @projects.search(params[:name]) if params[:name].present? @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]) diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 513348c39af..30df1fb2fec 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -27,10 +27,7 @@ class Import::GitlabProjectsController < Import::BaseController notice: "Project '#{@project.name}' is being imported." ) else - redirect_to( - new_import_gitlab_project_path, - alert: "Project could not be imported: #{@project.errors.full_messages.join(', ')}" - ) + redirect_back_or_default(options: { alert: "Project could not be imported: #{@project.errors.full_messages.join(', ')}" }) end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 7599fec3cdf..5356fdf010d 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -57,7 +57,7 @@ class Projects::BlobController < Projects::ApplicationController diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true) diff_lines = diffy.diff.scan(/.*\n/)[2..-1] diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines) - @diff_lines = Gitlab::Diff::Highlight.new(diff_lines).highlight + @diff_lines = Gitlab::Diff::Highlight.new(diff_lines, repository: @repository).highlight render layout: false end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index d162a5a3165..37d6521026c 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -121,7 +121,6 @@ class Projects::CommitController < Projects::ApplicationController opts[:ignore_whitespace_change] = true if params[:format] == 'diff' @diffs = commit.diffs(opts) - @diff_refs = [commit.parent || commit, commit] @notes_count = commit.notes.count @statuses = CommitStatus.where(pipeline: pipelines) diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index af0b69a2442..d240b9fe989 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -14,14 +14,22 @@ class Projects::CompareController < Projects::ApplicationController def show compare = CompareService.new. - execute(@project, @head_ref, @project, @base_ref, diff_options) + execute(@project, @head_ref, @project, @start_ref, diff_options) if compare @commits = Commit.decorate(compare.commits, @project) + + @start_commit = @project.commit(@start_ref) @commit = @project.commit(@head_ref) - @base_commit = @project.merge_base_commit(@base_ref, @head_ref) + @base_commit = @project.merge_base_commit(@start_ref, @head_ref) + @diffs = compare.diffs(diff_options) - @diff_refs = [@base_commit, @commit] + @diff_refs = Gitlab::Diff::DiffRefs.new( + base_sha: @base_commit.try(:sha), + start_sha: @start_commit.try(:sha), + head_sha: @commit.try(:sha) + ) + @diff_notes_disabled = true @grouped_diff_notes = {} end @@ -35,12 +43,12 @@ class Projects::CompareController < Projects::ApplicationController private def assign_ref_vars - @base_ref = Addressable::URI.unescape(params[:from]) + @start_ref = Addressable::URI.unescape(params[:from]) @ref = @head_ref = Addressable::URI.unescape(params[:to]) end def merge_request @merge_request ||= @project.merge_requests.opened. - find_by(source_project: @project, source_branch: @head_ref, target_branch: @base_ref) + find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index dd86b940a08..df1943dd9bb 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -58,14 +58,17 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html - format.json { render json: @merge_request } + + format.json do + render json: @merge_request + end + format.patch do - headers.store(*Gitlab::Workhorse.send_git_patch(@project.repository, - @merge_request.diff_base_commit.id, - @merge_request.last_commit.id)) - headers['Content-Disposition'] = 'inline' - head :ok + return render_404 unless @merge_request.diff_refs + + send_git_patch @project.repository, @merge_request.diff_refs end + format.diff do return render_404 unless @merge_request.diff_refs @@ -77,18 +80,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController def diffs apply_diff_view_cookie! - @commit = @merge_request.last_commit - @base_commit = @merge_request.diff_base_commit - - # MRs created before 8.4 don't have a diff_base_commit, - # but we need it for the "View file @ ..." link by deleted files - @base_commit ||= @merge_request.first_commit.parent || @merge_request.first_commit + @commit = @merge_request.diff_head_commit + @base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit @comments_target = { noteable_type: 'MergeRequest', noteable_id: @merge_request.id } + @use_legacy_diff_notes = !@merge_request.support_new_diff_notes? @grouped_diff_notes = @merge_request.notes.grouped_diff_notes Banzai::NoteRenderer.render( @@ -134,7 +134,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @target_project = merge_request.target_project @source_project = merge_request.source_project @commits = @merge_request.compare_commits.reverse - @commit = @merge_request.last_commit + @commit = @merge_request.diff_head_commit @base_commit = @merge_request.diff_base_commit @diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare @diff_notes_disabled = true @@ -212,7 +212,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController return end - if params[:sha] != @merge_request.source_sha + if params[:sha] != @merge_request.diff_head_sha @status = :sha_mismatch return end @@ -274,16 +274,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController status ||= "preparing" else ci_service = @merge_request.source_project.ci_service - status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service + status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service if ci_service.respond_to?(:commit_coverage) - coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch) + coverage = ci_service.commit_coverage(merge_request.diff_head_sha, merge_request.source_branch) end end response = { title: merge_request.title, - sha: merge_request.last_commit_short_sha, + sha: merge_request.diff_head_commit.short_id, status: status, coverage: coverage } diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index e14fe26dde7..3eacdbbd067 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -128,7 +128,7 @@ class Projects::NotesController < Projects::ApplicationController elsif note.valid? Banzai::NoteRenderer.render([note], @project, current_user) - { + attrs = { valid: true, id: note.id, discussion_id: note.discussion_id, @@ -138,6 +138,23 @@ class Projects::NotesController < Projects::ApplicationController discussion_html: note_to_discussion_html(note), discussion_with_diff_html: note_to_discussion_with_diff_html(note) } + + # The discussion_id is used to add the comment to the correct discussion + # element on the merge request page. Among other things, the discussion_id + # contains the sha of head commit of the merge request. + # When new commits are pushed into the merge request after the initial + # load of the merge request page, the discussion elements will still have + # the old discussion_ids, with the old head commit sha. The new comment, + # however, will have the new discussion_id with the new commit sha. + # To ensure that these new comments will still end up in the correct + # discussion element, we also send the original discussion_id, with the + # old commit sha, along, and fall back on this value when no discussion + # element with the new discussion_id could be found. + if note.new_diff_note? && note.position != note.original_position + attrs[:original_discussion_id] = note.original_discussion_id + end + + attrs else { valid: false, @@ -154,7 +171,7 @@ class Projects::NotesController < Projects::ApplicationController def note_params params.require(:note).permit( :note, :noteable, :noteable_id, :noteable_type, :project_id, - :attachment, :line_code, :commit_id, :type + :attachment, :line_code, :commit_id, :type, :position ) end diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index f240584ccbf..950f323e383 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -31,7 +31,7 @@ module AppearancesHelper end end - def navbar_icon(icon_name) - render "shared/icons/#{icon_name}.svg" + def navbar_icon(icon_name, size: 16) + render "shared/icons/#{icon_name}.svg", size: size end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 62d13a4b4f3..03495cf5ec4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -306,4 +306,15 @@ module ApplicationHelper def truncate_first_line(message, length = 50) truncate(message.each_line.first.chomp, length: length) if message end + + # While similarly named to Rails's `link_to_if`, this method behaves quite differently. + # If `condition` is truthy, a link will be returned with the result of the block + # as its body. If `condition` is falsy, only the result of the block will be returned. + def conditional_link_to(condition, options, html_options = {}, &block) + if condition + link_to options, html_options, &block + else + capture(&block) + end + end end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index e22dce59d0f..eb57516247d 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -30,12 +30,8 @@ module DiffHelper options end - def safe_diff_files(diffs, diff_refs) - diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs) } - end - - def generate_line_code(file_path, line) - Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) + def safe_diff_files(diffs, diff_refs: nil, repository: nil) + diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) } end def unfold_bottom_class(bottom) @@ -93,6 +89,8 @@ module DiffHelper end def commit_for_diff(diff_file) + return diff_file.content_commit if diff_file.content_commit + if diff_file.deleted_file @base_commit || @commit.parent || @commit else @@ -100,10 +98,11 @@ module DiffHelper end end - def diff_file_html_data(project, diff_commit, diff_file) + def diff_file_html_data(project, diff_file) + commit = commit_for_diff(diff_file) { blob_diff_path: namespace_project_blob_diff_path(project.namespace, project, - tree_join(diff_commit.id, diff_file.file_path)) + tree_join(commit.id, diff_file.file_path)) } end diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 7c140538012..4566f3782cc 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -39,7 +39,7 @@ module DropdownsHelper end end - def dropdown_toggle(toggle_text, data_attr, options) + def dropdown_toggle(toggle_text, data_attr, options = {}) content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do output = content_tag(:span, toggle_text, class: "dropdown-toggle-text") output << icon('chevron-down') diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 1dd07a2a220..c7dedfe9254 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -27,7 +27,7 @@ module MergeRequestsHelper end def ci_build_details_path(merge_request) - build_url = merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch) + build_url = merge_request.source_project.ci_service.build_page(merge_request.diff_head_sha, merge_request.source_branch) return nil unless build_url parsed_url = URI.parse(build_url) diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index e85ba76887d..2302e65c537 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -24,23 +24,55 @@ module NotesHelper }.to_json end - def link_to_new_diff_note(line_code, line_type = nil) - discussion_id = LegacyDiffNote.build_discussion_id( - @comments_target[:noteable_type], - @comments_target[:noteable_id] || @comments_target[:commit_id], - line_code - ) + def link_to_new_diff_note(line_code, position, line_type = nil) + use_legacy_diff_note = @use_legacy_diff_notes + # If the controller doesn't force the use of legacy diff notes, we + # determine this on a line-by-line basis by seeing if there already exist + # active legacy diff notes at this line, in which case newly created notes + # will use the legacy technology as well. + # We do this because the discussion_id values of legacy and "new" diff + # notes, which are used to group notes on the merge request discussion tab, + # are incompatible. + # If we didn't, diff notes that would show for the same line on the changes + # tab, would show in different discussions on the discussion tab. + use_legacy_diff_note ||= begin + line_diff_notes = @grouped_diff_notes[line_code] + line_diff_notes && line_diff_notes.any?(&:legacy_diff_note?) + end data = { noteable_type: @comments_target[:noteable_type], noteable_id: @comments_target[:noteable_id], commit_id: @comments_target[:commit_id], line_type: line_type, - line_code: line_code, - note_type: LegacyDiffNote.name, - discussion_id: discussion_id + line_code: line_code } + if use_legacy_diff_note + discussion_id = LegacyDiffNote.build_discussion_id( + @comments_target[:noteable_type], + @comments_target[:noteable_id] || @comments_target[:commit_id], + line_code + ) + + data.merge!( + note_type: LegacyDiffNote.name, + discussion_id: discussion_id + ) + else + discussion_id = DiffNote.build_discussion_id( + @comments_target[:noteable_type], + @comments_target[:noteable_id] || @comments_target[:commit_id], + position + ) + + data.merge!( + position: position.to_json, + note_type: DiffNote.name, + discussion_id: discussion_id + ) + end + button_tag(class: 'btn add-diff-note js-add-diff-note-button', data: data, title: 'Add a comment to this line') do @@ -60,14 +92,15 @@ module NotesHelper } if note.diff_note? - data.merge!( - line_code: note.line_code, - note_type: LegacyDiffNote.name - ) + data[:note_type] = note.type + + data.merge!(note.diff_attributes) end - button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', - data: data, title: 'Add a reply' + content_tag(:div, class: "discussion-reply-holder") do + button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', + data: data, title: 'Add a reply' + end end def note_max_access_for_user(note) @@ -79,4 +112,14 @@ module NotesHelper full_key = { project: note.project, user_id: note.author_id } @max_access_by_user_id[full_key] end + + def diff_note_path(note) + return unless note.diff_note? + + if note.for_merge_request? && note.active? + diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) + elsif note.for_commit? + namespace_project_commit_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) + end + end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 88787576dd3..3bbbb26cff2 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -293,7 +293,11 @@ module ProjectsHelper end def last_push_event - if current_user + return unless current_user + + if fork = current_user.fork_of(@project) + current_user.recent_push(fork.id) + else current_user.recent_push(@project.id) end end diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb index 2bd0dbfd095..65598ad9ed3 100644 --- a/app/helpers/workhorse_helper.rb +++ b/app/helpers/workhorse_helper.rb @@ -1,4 +1,4 @@ -# Helpers to send Git blobs, diffs or archives through Workhorse. +# Helpers to send Git blobs, diffs, patches or archives through Workhorse. # Workhorse will also serve files when using `send_file`. module WorkhorseHelper # Send a Git blob through Workhorse @@ -16,6 +16,13 @@ module WorkhorseHelper head :ok end + # Send a Git patch through Workhorse + def send_git_patch(repository, diff_refs) + headers.store(*Gitlab::Workhorse.send_git_patch(repository, diff_refs)) + headers['Content-Disposition'] = 'inline' + head :ok + end + # Archive a Git repository and send it through Workhorse def send_git_archive(repository, ref:, format:) headers.store(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format)) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index e0af7081411..236b6ab00d8 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -29,8 +29,7 @@ module Emails # used in notify layout @target_url = @message.target_url @project = Project.find(project_id) - @diff_notes_disabled = true - + add_project_headers headers['X-GitLab-Author'] = @message.author_username diff --git a/app/models/commit.rb b/app/models/commit.rb index 174ccbaea6c..2ef3973c160 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -214,6 +214,13 @@ class Commit @raw.short_id(7) end + def diff_refs + Gitlab::Diff::DiffRefs.new( + base_sha: self.parent_id || self.sha, + head_sha: self.sha + ) + end + def pipelines @pipeline ||= project.pipelines.where(sha: sha) end diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb new file mode 100644 index 00000000000..2785fbb21c9 --- /dev/null +++ b/app/models/concerns/note_on_diff.rb @@ -0,0 +1,52 @@ +module NoteOnDiff + extend ActiveSupport::Concern + + NUMBER_OF_TRUNCATED_DIFF_LINES = 16 + + included do + delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true + end + + def diff_note? + true + end + + def diff_file + raise NotImplementedError + end + + def diff_line + raise NotImplementedError + end + + def for_line?(line) + raise NotImplementedError + end + + def diff_attributes + raise NotImplementedError + end + + def can_be_award_emoji? + false + end + + # Returns an array of at most 16 highlighted lines above a diff note + def truncated_diff_lines + prev_lines = [] + + highlighted_diff_lines.each do |line| + if line.meta? + prev_lines.clear + else + prev_lines << line + + break if for_line?(line) + + prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES + end + end + + prev_lines + end +end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index e498ca96e3c..520026c18dd 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -11,6 +11,8 @@ class Deployment < ActiveRecord::Base delegate :name, to: :environment, prefix: true + after_save :keep_around_commit + def commit project.commit(sha) end @@ -26,4 +28,8 @@ class Deployment < ActiveRecord::Base def last? self == environment.last_deployment end + + def keep_around_commit + project.repository.keep_around(self.sha) + end end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb new file mode 100644 index 00000000000..9671955db36 --- /dev/null +++ b/app/models/diff_note.rb @@ -0,0 +1,127 @@ +class DiffNote < Note + include NoteOnDiff + + serialize :original_position, Gitlab::Diff::Position + serialize :position, Gitlab::Diff::Position + + validates :original_position, presence: true + validates :position, presence: true + validates :diff_line, presence: true + validates :line_code, presence: true, line_code: true + validates :noteable_type, inclusion: { in: ['Commit', 'MergeRequest'] } + validate :positions_complete + validate :verify_supported + + before_validation :set_original_position, :update_position, on: :create + before_validation :set_line_code + after_save :keep_around_commits + + class << self + def build_discussion_id(noteable_type, noteable_id, position) + [super(noteable_type, noteable_id), *position.key].join("-") + end + end + + def new_diff_note? + true + end + + def diff_attributes + { position: position.to_json } + end + + def discussion_id + @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, position) + end + + def original_discussion_id + @original_discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, original_position) + end + + def position=(new_position) + if new_position.is_a?(String) + new_position = JSON.parse(new_position) rescue nil + end + + if new_position.is_a?(Hash) + new_position = new_position.with_indifferent_access + new_position = Gitlab::Diff::Position.new(new_position) + end + + super(new_position) + end + + def diff_file + @diff_file ||= self.original_position.diff_file(self.project.repository) + end + + def diff_line + @diff_line ||= diff_file.line_for_position(self.original_position) if diff_file + end + + def for_line?(line) + diff_file.position(line) == self.original_position + end + + def active?(diff_refs = nil) + return false unless supported? + return true if for_commit? + + diff_refs ||= self.noteable.diff_refs + + self.position.diff_refs == diff_refs + end + + private + + def supported? + !self.for_merge_request? || self.noteable.support_new_diff_notes? + end + + def set_original_position + self.original_position = self.position.dup + end + + def set_line_code + self.line_code = self.position.line_code(self.project.repository) + end + + def update_position + return unless supported? + return if for_commit? + + return if active? + + Notes::DiffPositionUpdateService.new( + self.project, + nil, + old_diff_refs: self.position.diff_refs, + new_diff_refs: self.noteable.diff_refs, + paths: self.position.paths + ).execute(self) + end + + def verify_supported + return if supported? + + errors.add(:noteable, "doesn't support new-style diff notes") + end + + def positions_complete + return if self.original_position.complete? && self.position.complete? + + errors.add(:position, "is invalid") + end + + def keep_around_commits + project.repository.keep_around(self.original_position.base_sha) + project.repository.keep_around(self.original_position.start_sha) + project.repository.keep_around(self.original_position.head_sha) + + if self.position != self.original_position + project.repository.keep_around(self.position.base_sha) + project.repository.keep_around(self.position.start_sha) + project.repository.keep_around(self.position.head_sha) + end + end +end diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index 33d2a69ebaf..790dfd4d480 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -1,4 +1,6 @@ class LegacyDiffNote < Note + include NoteOnDiff + serialize :st_diff validates :line_code, presence: true, line_code: true @@ -11,12 +13,12 @@ class LegacyDiffNote < Note end end - def diff_note? + def legacy_diff_note? true end - def legacy_diff_note? - true + def diff_attributes + { line_code: line_code } end def discussion_id @@ -27,61 +29,20 @@ class LegacyDiffNote < Note line_code.split('_')[0] if line_code end - def diff_old_line - line_code.split('_')[1].to_i if line_code - end - - def diff_new_line - line_code.split('_')[2].to_i if line_code - end - def diff @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) end - def diff_file_path - diff.new_path.presence || diff.old_path - end - - def diff_lines - @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line) + def diff_file + @diff_file ||= Gitlab::Diff::File.new(diff, repository: self.project.repository) if diff end def diff_line - @diff_line ||= diff_lines.find { |line| generate_line_code(line) == self.line_code } + @diff_line ||= diff_file.line_for_line_code(self.line_code) end - def diff_line_text - diff_line.try(:text) - end - - def diff_line_type - diff_line.try(:type) - end - - def highlighted_diff_lines - Gitlab::Diff::Highlight.new(diff_lines).highlight - end - - def truncated_diff_lines - max_number_of_lines = 16 - prev_match_line = nil - prev_lines = [] - - highlighted_diff_lines.each do |line| - if line.type == "match" - prev_lines.clear - prev_match_line = line - else - prev_lines << line - - break if generate_line_code(line) == self.line_code - - prev_lines.shift if prev_lines.length >= max_number_of_lines - end - end - - prev_lines + def for_line?(line) + !line.meta? && diff_file.line_code(line) == self.line_code end # Check if this note is part of an "active" discussion @@ -102,7 +63,7 @@ class LegacyDiffNote < Note if noteable_diff parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line) - @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line_text } + @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line.text } else @active = false end @@ -110,10 +71,6 @@ class LegacyDiffNote < Note @active end - def award_emoji_supported? - false - end - private def find_diff @@ -149,10 +106,6 @@ class LegacyDiffNote < Note self.class.where(attributes).last.try(:diff) end - def generate_line_code(line) - Gitlab::Diff::LineCode.generate(diff_file_path, line.new_pos, line.old_pos) - end - # Find the diff on noteable that matches our own def find_noteable_diff diffs = noteable.diffs(Commit.max_diff_options) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 4f7e1d2f302..083e93f1ee7 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -16,7 +16,7 @@ class MergeRequest < ActiveRecord::Base serialize :merge_params, Hash - after_create :create_merge_request_diff, unless: :importing + after_create :create_merge_request_diff, unless: :importing? after_update :update_merge_request_diff delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil @@ -29,10 +29,6 @@ class MergeRequest < ActiveRecord::Base # when creating new merge request attr_accessor :can_be_created, :compare_commits, :compare - # Temporary fields to store target_sha, and base_sha to - # compare when importing pull requests from GitHub - attr_accessor :base_target_sha, :head_source_sha - state_machine :state, initial: :opened do event :close do transition [:reopened, :opened] => :closed @@ -89,12 +85,7 @@ class MergeRequest < ActiveRecord::Base state :cannot_be_merged around_transition do |merge_request, transition, block| - merge_request.record_timestamps = false - begin - block.call - ensure - merge_request.record_timestamps = true - end + Gitlab::Timeless.timeless(merge_request, &block) end end @@ -169,10 +160,6 @@ class MergeRequest < ActiveRecord::Base reference end - def last_commit - merge_request_diff ? merge_request_diff.last_commit : compare_commits.last - end - def first_commit merge_request_diff ? merge_request_diff.first_commit : compare_commits.first end @@ -182,15 +169,86 @@ class MergeRequest < ActiveRecord::Base end def diff_base_commit - if merge_request_diff + if persisted? merge_request_diff.base_commit - elsif source_sha - self.target_project.merge_base_commit(self.source_sha, self.target_branch) + elsif diff_start_commit && diff_head_commit + self.target_project.merge_base_commit(diff_start_sha, diff_head_sha) + 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.parent || first_commit + end + + def diff_start_commit + if persisted? + merge_request_diff.start_commit + else + target_branch_head + end + end + + def diff_head_commit + if persisted? + merge_request_diff.head_commit + else + source_branch_head end end - def last_commit_short_sha - last_commit.short_id + def diff_start_sha + diff_start_commit.try(:sha) + end + + def diff_base_sha + diff_base_commit.try(:sha) + end + + def diff_head_sha + diff_head_commit.try(:sha) + end + + # When importing a pull request from GitHub, the old and new branches may no + # longer actually exist by those names, but we need to recreate the merge + # request diff with the right source and target shas. + # We use these attributes to force these to the intended values. + attr_writer :target_branch_sha, :source_branch_sha + + def source_branch_head + source_branch_ref = @source_branch_sha || source_branch + source_project.repository.commit(source_branch) if source_branch_ref + end + + def target_branch_head + target_branch_ref = @target_branch_sha || target_branch + target_project.repository.commit(target_branch) if target_branch_ref + end + + def target_branch_sha + target_branch_head.try(:sha) + end + + def source_branch_sha + source_branch_head.try(:sha) + 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 def validate_branches @@ -227,21 +285,30 @@ class MergeRequest < ActiveRecord::Base def update_merge_request_diff if source_branch_changed? || target_branch_changed? - reload_code + reload_diff end end - def reload_code - if merge_request_diff && open? - merge_request_diff.reload_content - end + def reload_diff + return unless merge_request_diff && open? + + old_diff_refs = self.diff_refs + + merge_request_diff.reload_content + + new_diff_refs = self.diff_refs + + update_diff_notes_positions( + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs + ) end def check_if_can_be_merged return unless unchecked? can_be_merged = - !broken? && project.repository.can_be_merged?(source_sha, target_branch) + !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch) if can_be_merged mark_as_mergeable @@ -293,7 +360,7 @@ class MergeRequest < ActiveRecord::Base !source_project.protected_branch?(source_branch) && !source_project.root_ref?(source_branch) && Ability.abilities.allowed?(current_user, :push_code, source_project) && - last_commit == source_project.commit(source_branch) + diff_head_commit == source_branch_head end def should_remove_source_branch? @@ -331,8 +398,8 @@ class MergeRequest < ActiveRecord::Base work_in_progress: work_in_progress? } - if last_commit - attrs.merge!(last_commit: last_commit.hook_attrs) + if diff_head_commit + attrs.merge!(last_commit: diff_head_commit.hook_attrs) end attributes.merge!(attrs) @@ -510,22 +577,6 @@ class MergeRequest < ActiveRecord::Base end end - def target_sha - return @base_target_sha if defined?(@base_target_sha) - - target_project.repository.commit(target_branch).try(:sha) - end - - def source_sha - return @head_source_sha if defined?(@head_source_sha) - - last_commit.try(:sha) || source_tip.try(:sha) - end - - def source_tip - source_branch && source_project.repository.commit(source_branch) - end - def fetch_ref target_project.repository.fetch_ref( source_project.repository.path_to_repo, @@ -558,10 +609,10 @@ class MergeRequest < ActiveRecord::Base def diverged_commits_count cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits") - if cache.blank? || cache[:source_sha] != source_sha || cache[:target_sha] != target_sha + if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha cache = { - source_sha: source_sha, - target_sha: target_sha, + source_sha: source_branch_sha, + target_sha: target_branch_sha, diverged_commits_count: compute_diverged_commits_count } Rails.cache.write(:"merge_request_#{id}_diverged_commits", cache) @@ -571,9 +622,9 @@ class MergeRequest < ActiveRecord::Base end def compute_diverged_commits_count - return 0 unless source_sha && target_sha + return 0 unless source_branch_sha && target_branch_sha - Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size + Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size end private :compute_diverged_commits_count @@ -582,13 +633,7 @@ class MergeRequest < ActiveRecord::Base end def pipeline - @pipeline ||= source_project.pipeline(last_commit.id, source_branch) if last_commit && source_project - end - - def diff_refs - return nil unless diff_base_commit - - [diff_base_commit, last_commit] + @pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project end def merge_commit @@ -603,6 +648,36 @@ class MergeRequest < ActiveRecord::Base merge_commit end + def support_new_diff_notes? + diff_refs && diff_refs.complete? + end + + def update_diff_notes_positions(old_diff_refs:, new_diff_refs:) + return unless support_new_diff_notes? + return if new_diff_refs == old_diff_refs + + active_diff_notes = self.notes.diff_notes.select do |note| + note.new_diff_note? && note.active?(old_diff_refs) + end + + return if active_diff_notes.empty? + + paths = active_diff_notes.flat_map { |n| n.diff_file.paths }.uniq + + service = Notes::DiffPositionUpdateService.new( + self.project, + nil, + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs, + paths: paths + ) + + active_diff_notes.each do |note| + service.execute(note) + Gitlab::Timeless.timeless(note, &:save) + end + end + def keep_around_commit project.repository.keep_around(self.merge_commit_sha) end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 0fcde6fc8f1..ba235750aeb 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -7,7 +7,7 @@ class MergeRequestDiff < ActiveRecord::Base belongs_to :merge_request - delegate :head_source_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil + delegate :source_branch_sha, :target_branch_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil state_machine :state, initial: :empty do state :collected @@ -24,7 +24,7 @@ class MergeRequestDiff < ActiveRecord::Base serialize :st_diffs after_create :reload_content, unless: :importing? - after_save :keep_around_commit + after_save :keep_around_commits, unless: :importing? def reload_content reload_commits @@ -39,9 +39,9 @@ class MergeRequestDiff < ActiveRecord::Base if options[:ignore_whitespace_change] @diffs_no_whitespace ||= begin compare = Gitlab::Git::Compare.new( - self.repository.raw_repository, - self.base, - self.head, + repository.raw_repository, + self.start_commit_sha || self.target_branch_sha, + self.head_commit_sha || self.source_branch_sha, ) compare.diffs(options) end @@ -63,37 +63,39 @@ class MergeRequestDiff < ActiveRecord::Base end def base_commit - return nil unless self.base_commit_sha + return unless self.base_commit_sha - merge_request.target_project.commit(self.base_commit_sha) + project.commit(self.base_commit_sha) end - def last_commit_short_sha - @last_commit_short_sha ||= last_commit.short_id - end + def start_commit + return unless self.start_commit_sha - def dump_commits(commits) - commits.map(&:to_hash) + project.commit(self.start_commit_sha) end - def load_commits(array) - array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) } - end + def head_commit + return last_commit unless self.head_commit_sha - def dump_diffs(diffs) - if diffs.respond_to?(:map) - diffs.map(&:to_hash) - end + project.commit(self.head_commit_sha) end - def load_diffs(raw, options) - if raw.respond_to?(:each) - Gitlab::Git::DiffCollection.new(raw, options) - else - Gitlab::Git::DiffCollection.new([]) - end + def compare + @compare ||= + begin + # Update ref for merge request + merge_request.fetch_ref + + Gitlab::Git::Compare.new( + repository.raw_repository, + self.target_branch_sha, + self.source_branch_sha + ) + end end + private + # Collect array of Git::Commit objects # between target and source branches def unmerged_commits @@ -106,6 +108,14 @@ class MergeRequestDiff < ActiveRecord::Base commits end + def dump_commits(commits) + commits.map(&:to_hash) + end + + def load_commits(array) + array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) } + end + # Reload all commits related to current merge request from repo # and save it as array of hashes in st_commits db field def reload_commits @@ -120,6 +130,26 @@ class MergeRequestDiff < ActiveRecord::Base update_columns_serialized(new_attributes) end + # Collect array of Git::Diff objects + # between target and source branches + def unmerged_diffs + compare.diffs(Commit.max_diff_options) + end + + def dump_diffs(diffs) + if diffs.respond_to?(:map) + diffs.map(&:to_hash) + end + end + + def load_diffs(raw, options) + if raw.respond_to?(:each) + Gitlab::Git::DiffCollection.new(raw, options) + else + Gitlab::Git::DiffCollection.new([]) + end + end + # Reload diffs between branches related to current merge request from repo # and save it as array of hashes in st_diffs db field def reload_diffs @@ -147,59 +177,33 @@ class MergeRequestDiff < ActiveRecord::Base new_attributes[:st_diffs] = new_diffs - base_commit_sha = self.repository.merge_base(self.head, self.base) - new_attributes[:base_commit_sha] = base_commit_sha - - self.repository.keep_around(base_commit_sha) + new_attributes[:start_commit_sha] = self.target_branch_sha + new_attributes[:head_commit_sha] = self.source_branch_sha + new_attributes[:base_commit_sha] = branch_base_sha update_columns_serialized(new_attributes) - end - # Collect array of Git::Diff objects - # between target and source branches - def unmerged_diffs - compare.diffs(Commit.max_diff_options) + keep_around_commits end - def repository - merge_request.target_project.repository + def project + merge_request.target_project end - def source_sha - return head_source_sha if head_source_sha.present? - - source_commit = merge_request.source_project.commit(source_branch) - source_commit.try(:sha) + def repository + project.repository end - def target_sha - merge_request.target_sha - end + def branch_base_commit + return unless self.source_branch_sha && self.target_branch_sha - def base - self.target_sha || self.target_branch + project.merge_base_commit(self.source_branch_sha, self.target_branch_sha) end - def head - self.source_sha + def branch_base_sha + branch_base_commit.try(:sha) end - def compare - @compare ||= - begin - # Update ref for merge request - merge_request.fetch_ref - - Gitlab::Git::Compare.new( - self.repository.raw_repository, - self.base, - self.head - ) - end - end - - private - # # #save or #update_attributes providing changes on serialized attributes do a lot of # serialization and deserialization calls resulting in bad performance. @@ -223,7 +227,9 @@ class MergeRequestDiff < ActiveRecord::Base reload end - def keep_around_commit - self.repository.keep_around(self.base_commit_sha) + def keep_around_commits + repository.keep_around(target_branch_sha) + repository.keep_around(source_branch_sha) + repository.keep_around(branch_base_sha) end end diff --git a/app/models/note.rb b/app/models/note.rb index 81b5c47b738..ffffd0c0838 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -56,7 +56,7 @@ class Note < ActiveRecord::Base scope :inc_author, ->{ includes(:author) } scope :inc_author_project_award_emoji, ->{ includes(:project, :author, :award_emoji) } - scope :legacy_diff_notes, ->{ where(type: 'LegacyDiffNote') } + scope :diff_notes, ->{ where(type: ['LegacyDiffNote', 'DiffNote']) } scope :non_diff_notes, ->{ where(type: ['Note', nil]) } scope :with_associations, -> do @@ -82,7 +82,7 @@ class Note < ActiveRecord::Base end def grouped_diff_notes - legacy_diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) + diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) end # Searches for notes matching the given query. @@ -115,6 +115,10 @@ class Note < ActiveRecord::Base false end + def new_diff_note? + false + end + def active? true end @@ -193,7 +197,7 @@ class Note < ActiveRecord::Base end def award_emoji? - award_emoji_supported? && contains_emoji_only? + can_be_award_emoji? && contains_emoji_only? end def emoji_awardable? @@ -204,7 +208,7 @@ class Note < ActiveRecord::Base self.line_code = nil if self.line_code.blank? end - def award_emoji_supported? + def can_be_award_emoji? noteable.is_a?(Awardable) end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index d41fc7073c6..121b598b8f3 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -5,6 +5,7 @@ class NotificationSetting < ActiveRecord::Base belongs_to :user belongs_to :source, polymorphic: true + belongs_to :project, foreign_key: 'source_id' validates :user, presence: true validates :level, presence: true @@ -13,7 +14,13 @@ class NotificationSetting < ActiveRecord::Base allow_nil: true } scope :for_groups, -> { where(source_type: 'Namespace') } - scope :for_projects, -> { where(source_type: 'Project') } + + # Exclude projects not included by the Project model's default scope (those that are + # pending delete). + # + scope :for_projects, -> do + includes(:project).references(:projects).where(source_type: 'Project').where.not(projects: { id: nil }) + end EMAIL_EVENTS = [ :new_note, diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 27bf08bf7d9..97bcbacf2b9 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -144,7 +144,7 @@ class JiraService < IssueTrackerService commit_id = if entity.is_a?(Commit) entity.id elsif entity.is_a?(MergeRequest) - entity.last_commit.id + entity.diff_head_sha end commit_url = build_entity_url(:commit, commit_id) diff --git a/app/models/repository.rb b/app/models/repository.rb index 078ca8f4e13..ba66bc47c29 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -653,16 +653,6 @@ class Repository end end - def blob_for_diff(commit, diff) - blob_at(commit.id, diff.file_path) - end - - def prev_blob_for_diff(commit, diff) - if commit.parent_id - blob_at(commit.parent_id, diff.old_path) - end - end - def refs_contains_sha(ref_type, sha) args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha}) names = Gitlab::Popen.popen(args, path_to_repo).first @@ -911,7 +901,7 @@ class Repository if line =~ /^.*:.*:\d+:/ ref, filename, startline = line.split(':') startline = startline.to_i - index - extname = File.extname(filename) + extname = Regexp.escape(File.extname(filename)) basename = filename.sub(/#{extname}$/, '') break end diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index a2df899d012..016172c6d7e 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -1,4 +1,6 @@ class SentNotification < ActiveRecord::Base + serialize :position, Gitlab::Diff::Position + belongs_to :project belongs_to :noteable, polymorphic: true belongs_to :recipient, class_name: "User" @@ -7,7 +9,7 @@ class SentNotification < ActiveRecord::Base validates :reply_key, uniqueness: true validates :noteable_id, presence: true, unless: :for_commit? validates :commit_id, presence: true, if: :for_commit? - validates :line_code, line_code: true, allow_blank: true + validate :note_valid after_save :keep_around_commit @@ -20,7 +22,7 @@ class SentNotification < ActiveRecord::Base find_by(reply_key: reply_key) end - def record(noteable, recipient_id, reply_key, params = {}) + def record(noteable, recipient_id, reply_key, attrs = {}) return unless reply_key noteable_id = nil @@ -31,7 +33,7 @@ class SentNotification < ActiveRecord::Base noteable_id = noteable.id end - params.reverse_merge!( + attrs.reverse_merge!( project: noteable.project, noteable_type: noteable.class.name, noteable_id: noteable_id, @@ -40,13 +42,17 @@ class SentNotification < ActiveRecord::Base reply_key: reply_key ) - create(params) + create(attrs) end - def record_note(note, recipient_id, reply_key, params = {}) - params[:line_code] = note.line_code + def record_note(note, recipient_id, reply_key, attrs = {}) + if note.diff_note? + attrs[:note_type] = note.type + + attrs.merge!(note.diff_attributes) + end - record(note.noteable, recipient_id, reply_key, params) + record(note.noteable, recipient_id, reply_key, attrs) end end @@ -70,8 +76,33 @@ class SentNotification < ActiveRecord::Base self.reply_key end + def note_attributes + { + project: self.project, + author: self.recipient, + type: self.note_type, + noteable_type: self.noteable_type, + noteable_id: self.noteable_id, + commit_id: self.commit_id, + line_code: self.line_code, + position: self.position.to_json + } + end + + def create_note(note) + Notes::CreateService.new( + self.project, + self.recipient, + self.note_attributes.merge(note: note) + ).execute + end + private + def note_valid + Note.new(note_attributes.merge(note: "Test")).valid? + end + def keep_around_commit project.repository.keep_around(self.commit_id) end diff --git a/app/services/audit_event_service.rb b/app/services/audit_event_service.rb index a7f090655e1..8a000585e89 100644 --- a/app/services/audit_event_service.rb +++ b/app/services/audit_event_service.rb @@ -7,7 +7,7 @@ class AuditEventService @details = { with: @details[:with], target_id: @author.id, - target_type: "User", + target_type: 'User', target_details: @author.name, } diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index e57b95f21ec..e294a962352 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -20,9 +20,11 @@ module Auth token.issuer = registry.issuer token.audience = AUDIENCE token.expire_time = token_expire_at + token[:access] = names.map do |name| { type: 'repository', name: name, actions: %w(*) } end + token.encoded end diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index cc128563437..d874582d54f 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -3,17 +3,20 @@ require_relative 'base_service' class CreateBranchService < BaseService def execute(branch_name, ref, source_project: @project) valid_branch = Gitlab::GitRefValidator.validate(branch_name) - if valid_branch == false + + unless valid_branch return error('Branch name is invalid') end repository = project.repository existing_branch = repository.find_branch(branch_name) + if existing_branch return error('Branch already exists') end new_branch = nil + if source_project != @project repository.with_tmp_ref do |tmp_ref| repository.fetch_ref( @@ -29,7 +32,6 @@ class CreateBranchService < BaseService end if new_branch - # GitPushService handles execution of services and hooks for branch pushes success(new_branch) else error('Invalid reference name') @@ -39,8 +41,6 @@ class CreateBranchService < BaseService end def success(branch) - out = super() - out[:branch] = branch - out + super().merge(branch: branch) end end diff --git a/app/services/create_release_service.rb b/app/services/create_release_service.rb index f029db72d40..d6d4afcf29a 100644 --- a/app/services/create_release_service.rb +++ b/app/services/create_release_service.rb @@ -23,8 +23,6 @@ class CreateReleaseService < BaseService end def success(release) - out = super() - out[:release] = release - out + super().merge(release: release) end end diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb index 9884cb96661..95cc9baf406 100644 --- a/app/services/create_snippet_service.rb +++ b/app/services/create_snippet_service.rb @@ -1,10 +1,10 @@ class CreateSnippetService < BaseService def execute - if project.nil? - snippet = PersonalSnippet.new(params) - else - snippet = project.snippets.build(params) - end + snippet = if project + project.snippets.build(params) + else + PersonalSnippet.new(params) + end unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) deny_visibility_level(snippet) diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index bd8d982e1fb..c0e7ecf6a96 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -9,6 +9,7 @@ class CreateTagService < BaseService message.strip! if message new_tag = nil + begin new_tag = repository.add_tag(current_user, tag_name, target, message) rescue Rugged::TagError @@ -22,6 +23,7 @@ class CreateTagService < BaseService CreateReleaseService.new(@project, @current_user). execute(tag_name, release_description) end + success.merge(tag: new_tag) else error("Target #{target} is invalid") diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 752a7029952..332c55581a1 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -5,7 +5,6 @@ class DeleteBranchService < BaseService repository = project.repository branch = repository.find_branch(branch_name) - # No such branch unless branch return error('No such branch', 404) end @@ -14,18 +13,15 @@ class DeleteBranchService < BaseService return error('Cannot remove HEAD branch', 405) end - # Dont allow remove of protected branch if project.protected_branch?(branch_name) return error('Protected branch cant be removed', 405) end - # Dont allow user to remove branch if he is not allowed to push unless current_user.can?(:push_code, project) return error('You dont have push access to repo', 405) end if repository.rm_branch(current_user, branch_name) - # GitPushService handles execution of services and hooks for branch pushes success('Branch was removed') else error('Failed to remove branch') @@ -35,15 +31,11 @@ class DeleteBranchService < BaseService end def error(message, return_code = 400) - out = super(message) - out[:return_code] = return_code - out + super(message).merge(return_code: return_code) end def success(message) - out = super() - out[:message] = message - out + super().merge(message: message) end def build_push_data(branch) diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb index de3352a6756..1e41fbe34b6 100644 --- a/app/services/delete_tag_service.rb +++ b/app/services/delete_tag_service.rb @@ -5,7 +5,6 @@ class DeleteTagService < BaseService repository = project.repository tag = repository.find_tag(tag_name) - # No such tag unless tag return error('No such tag', 404) end @@ -26,15 +25,11 @@ class DeleteTagService < BaseService end def error(message, return_code = 400) - out = super(message) - out[:return_code] = return_code - out + super(message).merge(return_code: return_code) end def success(message) - out = super() - out[:message] = message - out + super().merge(message: message) end def build_push_data(tag) diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 4bdb68a3698..37c5e321b39 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -15,7 +15,6 @@ module Files params[:file_content] end - # Validate parameters validate # Create new branch if it different from source_branch @@ -26,7 +25,7 @@ module Files if commit success else - error("Something went wrong. Your changes were not committed") + error('Something went wrong. Your changes were not committed') end rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, ValidationError => ex error(ex.message) @@ -51,12 +50,12 @@ module Files unless project.empty_repo? unless @source_project.repository.branch_names.include?(@source_branch) - raise_error("You can only create or edit files when you are on a branch") + raise_error('You can only create or edit files when you are on a branch') end if different_branch? if repository.branch_names.include?(@target_branch) - raise_error("Branch with such name already exists. You need to switch to this branch in order to make changes") + raise_error('Branch with such name already exists. You need to switch to this branch in order to make changes') end end end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index e4cde4a2fd8..8eaf6db8012 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -29,7 +29,7 @@ module Files blob = repository.blob_at_branch(@source_branch, @file_path) if blob - raise_error("Your changes could not be committed because a file with the same name already exists") + raise_error('Your changes could not be committed because a file with the same name already exists') end end end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 299a0a967b0..58573078048 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -26,6 +26,7 @@ class GitTagPushService < BaseService unless Gitlab::Git.blank_ref?(params[:newrev]) tag_name = Gitlab::Git.ref_name(params[:ref]) tag = project.repository.find_tag(tag_name) + if tag && tag.target == params[:newrev] commit = project.commit(tag.target) commits = [commit].compact diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb index 15825b81685..cd08c3a0cb9 100644 --- a/app/services/issues/bulk_update_service.rb +++ b/app/services/issues/bulk_update_service.rb @@ -9,6 +9,7 @@ module Issues end issues = Issue.where(id: issues_ids) + issues.each do |issue| next unless can?(current_user, :update_issue, issue) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 3bec66cea88..f1b1d90c457 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -34,7 +34,7 @@ module MergeRequests committer: committer } - commit_id = repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options) + commit_id = repository.merge(current_user, merge_request.diff_head_sha, merge_request.target_branch, options) merge_request.update(merge_commit_sha: commit_id) rescue GitHooksService::PreReceiveError => e merge_request.update(merge_error: e.message) diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb index 870f5705184..4ad5fb08311 100644 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -12,7 +12,7 @@ module MergeRequests merge_request.merge_when_build_succeeds = true merge_request.merge_user = @current_user - SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.last_commit) + SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit) end merge_request.save diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb index 064910f81f7..8437d9b8b43 100644 --- a/app/services/merge_requests/post_merge_service.rb +++ b/app/services/merge_requests/post_merge_service.rb @@ -20,6 +20,7 @@ module MergeRequests return unless merge_request.target_branch == project.default_branch closed_issues = merge_request.closes_issues(current_user) + closed_issues.each do |issue| if can?(current_user, :update_issue, issue) Issues::CloseService.new(project, current_user, {}).execute(issue, commit: merge_request) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index fe0579744b4..21490ac77ea 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -34,10 +34,10 @@ module MergeRequests def close_merge_requests commit_ids = @commits.map(&:id) merge_requests = @project.merge_requests.opened.where(target_branch: @branch_name).to_a - merge_requests = merge_requests.select(&:last_commit) + merge_requests = merge_requests.select(&:diff_head_commit) merge_requests = merge_requests.select do |merge_request| - commit_ids.include?(merge_request.last_commit.id) + commit_ids.include?(merge_request.diff_head_sha) end merge_requests.uniq.select(&:source_project).each do |merge_request| @@ -60,7 +60,7 @@ module MergeRequests merge_requests.each do |merge_request| if merge_request.source_branch == @branch_name || force_push? - merge_request.reload_code + merge_request.reload_diff merge_request.mark_as_unchecked else mr_commit_ids = merge_request.commits.map(&:id) @@ -68,7 +68,7 @@ module MergeRequests matches = mr_commit_ids & push_commit_ids if matches.any? - merge_request.reload_code + merge_request.reload_diff merge_request.mark_as_unchecked else merge_request.mark_as_unchecked @@ -94,12 +94,10 @@ module MergeRequests merge_request = merge_requests_for_source_branch.first return unless merge_request - last_commit = merge_request.last_commit - begin # Since any number of commits could have been made to the restored branch, # find the common root to see what has been added. - common_ref = @project.repository.merge_base(last_commit.id, @newrev) + common_ref = @project.repository.merge_base(merge_request.diff_head_sha, @newrev) # If the a commit no longer exists in this repo, gitlab_git throws # a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52 @commits = @project.repository.commits_between(common_ref, @newrev) if common_ref diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb index 8279ad2001b..eb88ae9d11c 100644 --- a/app/services/merge_requests/reopen_service.rb +++ b/app/services/merge_requests/reopen_service.rb @@ -6,7 +6,7 @@ module MergeRequests create_note(merge_request) notification_service.reopen_mr(merge_request, current_user) execute_hooks(merge_request, 'reopen') - merge_request.reload_code + merge_request.reload_diff merge_request.mark_as_unchecked end diff --git a/app/services/notes/diff_position_update_service.rb b/app/services/notes/diff_position_update_service.rb new file mode 100644 index 00000000000..0cb731f5bc3 --- /dev/null +++ b/app/services/notes/diff_position_update_service.rb @@ -0,0 +1,30 @@ +module Notes + class DiffPositionUpdateService < BaseService + def execute(note) + new_position = tracer.trace(note.position) + + # Don't update the position if the type doesn't match, since that means + # the diff line commented on was changed, and the comment is now outdated + old_position = note.position + if new_position && + new_position != old_position && + new_position.type == old_position.type + + note.position = new_position + end + + note + end + + private + + def tracer + @tracer ||= Gitlab::Diff::PositionTracer.new( + repository: project.repository, + old_diff_refs: params[:old_diff_refs], + new_diff_refs: params[:new_diff_refs], + paths: params[:paths] + ) + end + end +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 590350a11e5..ab6e51209ee 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -153,6 +153,7 @@ class NotificationService else mentioned_users end + recipients = recipients.concat(participants) # Merge project watchers @@ -176,6 +177,7 @@ class NotificationService # build notify method like 'note_commit_email' notify_method = "note_#{note.noteable_type.underscore}_email".to_sym + recipients.each do |recipient| mailer.send(notify_method, recipient.id, note.id).deliver_later end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 941df08995c..f06311511cc 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -3,10 +3,11 @@ module Projects def execute # check that user is allowed to set specified visibility_level new_visibility = params[:visibility_level] + if new_visibility && new_visibility.to_i != project.visibility_level unless can?(current_user, :change_visibility_level, project) && Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) - + deny_visibility_level(project, new_visibility) return project end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index b868d2e7e83..1ab3b5789bc 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -82,7 +82,7 @@ class SystemNoteService end body << ' ' << 'label'.pluralize(labels_count) - body = "#{body.capitalize}" + body = body.capitalize create_note(noteable: noteable, project: project, author: author, note: body) end @@ -125,7 +125,7 @@ class SystemNoteService # Returns the created Note object def self.change_status(noteable, project, author, status, source) body = "Status changed to #{status}" - body += " by #{source.gfm_reference(project)}" if source + body << " by #{source.gfm_reference(project)}" if source create_note(noteable: noteable, project: project, author: author, note: body) end @@ -139,7 +139,7 @@ class SystemNoteService # Called when 'merge when build succeeds' is canceled def self.cancel_merge_when_build_succeeds(noteable, project, author) - body = "Canceled the automatic merge" + body = 'Canceled the automatic merge' create_note(noteable: noteable, project: project, author: author, note: body) end @@ -236,6 +236,7 @@ class SystemNoteService else 'deleted' end + body = "#{verb} #{branch_type.to_s} branch `#{branch}`".capitalize create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb index 0c0f68d169b..0ee1ff2d7d9 100644 --- a/app/services/update_release_service.rb +++ b/app/services/update_release_service.rb @@ -21,8 +21,6 @@ class UpdateReleaseService < BaseService end def success(release) - out = super() - out[:release] = release - out + super().merge(release: release) end end diff --git a/app/services/update_snippet_service.rb b/app/services/update_snippet_service.rb index 93af8f21972..a6bb36821c3 100644 --- a/app/services/update_snippet_service.rb +++ b/app/services/update_snippet_service.rb @@ -9,6 +9,7 @@ class UpdateSnippetService < BaseService def execute # check that user is allowed to set specified visibility_level new_visibility = params[:visibility_level] + if new_visibility && new_visibility.to_i != snippet.visibility_level unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) deny_visibility_level(snippet, new_visibility) diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 9025aaac097..59fd6c3fea0 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -1,28 +1,20 @@ - css_class = '' unless local_assigns[:css_class] -- css_class += ' no-description' if group.description.blank? -%li.group-row{ class: css_class } - .controls.hidden-xs - = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn btn-grouped btn-sm' - = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: 'btn btn-grouped btn-sm btn-remove' +%li.group-row.group-admin{ class: css_class } + .group-avatar + = image_tag group_icon(group), class: 'avatar hidden-xs' + .group-details + .title + = link_to [:admin, group], class: 'group-name' do + = group.name + .group-stats + %span>= pluralize(number_with_delimiter(group.projects.count), 'project') + , + %span= pluralize(number_with_delimiter(group.users.count), 'member') - .stats - %span - = icon('bookmark') - = number_with_delimiter(group.projects.count) - - %span - = icon('users') - = number_with_delimiter(group.users.count) - - %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} - = visibility_level_icon(group.visibility_level, fw: false) - - = image_tag group_icon(group), class: 'avatar s40 hidden-xs' - .title - = link_to [:admin, group], class: 'group-name' do - = group.name - - - if group.description.present? - .description - = markdown(group.description, pipeline: :description) + - if group.description.present? + .description + = markdown(group.description, pipeline: :description) + .group-controls.hidden-xs + = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn' + = link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove' diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 94aa5f5a942..794f910a61f 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -3,41 +3,32 @@ = render "admin/dashboard/head" %div{ class: container_class } - %h3.page-title - Groups (#{number_with_delimiter(@groups.total_count)}) - - %p.light - Group allows you to keep projects organized. - Use groups for uniting related projects. - .top-area - .nav-search - = form_tag admin_groups_path, method: :get, class: 'form-inline' do + .prepend-top-default.append-bottom-default + = form_tag admin_groups_path, method: :get, class: 'js-search-form' do |f| = hidden_field_tag :sort, @sort - = text_field_tag :name, params[:name], class: "form-control" - = button_tag "Search", class: "btn submit btn-primary" - - .nav-controls - .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - %b.caret - %ul.dropdown-menu - %li - = link_to admin_groups_path(sort: sort_value_recently_created) do - = sort_title_recently_created - = link_to admin_groups_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to admin_groups_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to admin_groups_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated - = link_to 'New Group', new_admin_group_path, class: "btn btn-new" - + .search-holder + - project_name = params[:name].present? ? params[:name] : nil + .search-field-holder + = search_field_tag :name, project_name, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name' + = icon("search", class: "search-icon") + .dropdown + - toggle_text = @sort.present? ? sort_options_hash[@sort] : sort_title_recently_created + = dropdown_toggle(toggle_text, { toggle: 'dropdown' }) + %ul.dropdown-menu.dropdown-menu-align-right + %li.dropdown-header + Sort by + %li + = link_to admin_groups_path(sort: sort_value_recently_created, name: project_name) do + = sort_title_recently_created + = link_to admin_groups_path(sort: sort_value_oldest_created, name: project_name) do + = sort_title_oldest_created + = link_to admin_groups_path(sort: sort_value_recently_updated, name: project_name) do + = sort_title_recently_updated + = link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do + = sort_title_oldest_updated + = link_to new_admin_group_path, class: "btn btn-new" do + New Group %ul.content-list = render @groups diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 7d2eb423223..7fbce25b2c4 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -1,97 +1,94 @@ - @no_container = true - page_title "Projects" -= render 'shared/show_aside' +- params[:visibility_level] ||= [] + = render "admin/dashboard/head" %div{ class: container_class } - .row.prepend-top-default - %aside.col-md-3 - .panel.admin-filter - = form_tag admin_namespaces_projects_path, method: :get, class: '' do - .form-group - = label_tag :name, 'Name:' - = text_field_tag :name, params[:name], class: "form-control" + .top-area + .prepend-top-default + = form_tag admin_namespaces_projects_path, method: :get do |f| + .search-holder + .search-field-holder + = search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name' + + - if params[:visibility_level].present? + = hidden_field_tag 'visibility_level', params[:visibility_level] + + - if params[:sort].present? + = hidden_field_tag 'sort', params[:sort] + + - if params[:personal].present? + = hidden_field_tag 'visibility_level', 'true' + + - if params[:archived].present? + = hidden_field_tag 'archived', 'true' + + = icon("search", class: "search-icon") + + .dropdown + - toggle_text = 'Search for Namespace' + - if params[:namespace_id].present? + - namespace = Namespace.find(params[:namespace_id]) + - toggle_text = "#{namespace.kind}: #{namespace.path}" + = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' }) + .dropdown-menu.dropdown-select.dropdown-menu-align-right + = dropdown_title('Namespaces') + = dropdown_filter("Search for Namespace") + = dropdown_content + = dropdown_loading + + = button_tag "Search", class: "btn btn-primary btn-search" + + %ul.nav-links + - opts = params[:visibility_level].present? ? {} : { page: admin_namespaces_projects_path } + = nav_link(opts) do + = link_to admin_namespaces_projects_path do + All - .form-group - = label_tag :namespace_id, "Namespace" - = namespace_select_tag :namespace_id, selected: params[:namespace_id], class: 'input-large' + = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do + = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do + Private + = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do + = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do + Internal + = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do + = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do + Public - .form-group - %strong Activity - .checkbox - = label_tag :with_push do - = check_box_tag :with_push, 1, params[:with_push] - %span Projects with push events - .checkbox - = label_tag :abandoned do - = check_box_tag :abandoned, 1, params[:abandoned] - %span No activity over 6 month - .checkbox - = label_tag :with_archived do - = check_box_tag :with_archived, 1, params[:with_archived] - %span Show archived projects + .nav-controls + = render 'shared/projects/dropdown' + = link_to new_project_path, class: 'btn btn-new' do + New Project - %fieldset - %strong Visibility level: - .visibility-levels - - Project.visibility_levels.each do |label, level| - .checkbox - %label - = check_box_tag 'visibility_levels[]', level, params[:visibility_levels].present? && params[:visibility_levels].include?(level.to_s) - %span.descr - = visibility_level_icon(level) - = label - %fieldset - %strong Problems - .checkbox - = label_tag :last_repository_check_failed do - = check_box_tag :last_repository_check_failed, 1, params[:last_repository_check_failed] - %span Last repository check failed + .projects-list-holder + - if @projects.any? + %ul.projects-list.content-list + - @projects.each_with_index do |project| + %li.project-row + .controls.pull-right + - if project.archived + %span.label.label-warning archived + %span.label.label-gray + = repository_size(project) + = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn" + = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove" + .title + = link_to [:admin, project.namespace.becomes(Namespace), project] do + .dash-project-avatar + = project_icon(project, alt: '', class: 'avatar project-avatar s40') + %span.project-full-name + %span.namespace-name + - if project.namespace + = project.namespace.human_name + \/ + %span.project-name.filter-title + = project.name - = hidden_field_tag :sort, params[:sort] - = button_tag "Search", class: "btn submit btn-primary" - = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel" + - if project.description.present? + .description + = markdown(project.description, pipeline: :description) - %section.col-md-9 - .panel.panel-default - .panel-heading - Projects (#{@projects.total_count}) - .controls - .dropdown.inline - %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - %b.caret - %ul.dropdown-menu - %li - = link_to admin_namespaces_projects_path(sort: sort_value_recently_created) do - = sort_title_recently_created - = link_to admin_namespaces_projects_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to admin_namespaces_projects_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to admin_namespaces_projects_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated - = link_to admin_namespaces_projects_path(sort: sort_value_largest_repo) do - = sort_title_largest_repo - = link_to 'New Project', new_project_path, class: "btn btn-sm btn-success" - %ul.well-list - - @projects.each do |project| - %li - .list-item-name - %span{ class: visibility_level_color(project.visibility_level) } - = visibility_level_icon(project.visibility_level) - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] - .pull-right - - if project.archived - %span.label.label-warning archived - %span.label.label-gray - = repository_size(project) - = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" - = link_to 'Destroy', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-sm btn-remove" - - if @projects.blank? - .nothing-here-block 0 projects matches - = paginate @projects, theme: "gitlab" + = paginate @projects, theme: 'gitlab' + - else + .nothing-here-block No projects found diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 82d3169c6f9..2c5aba71699 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -99,7 +99,13 @@ .form-group = f.label :new_namespace_id, "Namespace", class: 'control-label' .col-sm-10 - = namespace_select_tag :new_namespace_id, selected: params[:namespace_id], class: 'input-large' + .dropdown + = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id', show_any: 'false' }, { toggle_class: 'js-namespace-select large' }) + .dropdown-menu.dropdown-select + = dropdown_title('Namespaces') + = dropdown_filter("Search for Namespace") + = dropdown_content + = dropdown_loading .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml new file mode 100644 index 00000000000..d3519f616f6 --- /dev/null +++ b/app/views/admin/users/_user.html.haml @@ -0,0 +1,42 @@ +%li.user-row + .user-avatar + = image_tag avatar_icon(user), class: "avatar", alt: '' + .user-details + .user-name + = link_to user.name, [:admin, user] + - if user.blocked? + %span.label.label-danger blocked + - if user.admin? + %span.label.label-success Admin + - if user.external? + %span.label.label-default External + - if user == current_user + %span It's you! + .user-email + = mail_to user.email, user.email + .controls.pull-right + = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn' + - unless user == current_user + .dropdown.inline + %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', data: { toggle: 'dropdown' } } + = icon('cog') + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right + %li.dropdown-header + Settings + %li + - if user.ldap_blocked? + %span.small Cannot unblock LDAP blocked users + - elsif user.blocked? + = link_to 'Unblock', unblock_admin_user_path(user), method: :put + - else + = link_to 'Block', block_admin_user_path(user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put + - if user.access_locked? + %li + = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } + - if user.can_be_removed? + %li.divider + %li + = link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" }, + class: 'btn btn-remove btn-block', + method: :delete diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 21bb99a792c..357123c2c13 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -1,110 +1,78 @@ - @no_container = true - page_title "Users" -= render 'shared/show_aside' = render "admin/dashboard/head" %div{ class: container_class } - .admin-filter - %ul.nav-links - %li{class: "#{'active' unless params[:filter]}"} - = link_to admin_users_path do - Active - %small.badge= number_with_delimiter(User.active.count) - %li{class: "#{'active' if params[:filter] == "admins"}"} - = link_to admin_users_path(filter: "admins") do - Admins - %small.badge= number_with_delimiter(User.admins.count) - %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"} - = link_to admin_users_path(filter: 'two_factor_enabled') do - 2FA Enabled - %small.badge= number_with_delimiter(User.with_two_factor.count) - %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"} - = link_to admin_users_path(filter: 'two_factor_disabled') do - 2FA Disabled - %small.badge= number_with_delimiter(User.without_two_factor.count) - %li.filter-external{class: "#{'active' if params[:filter] == 'external'}"} - = link_to admin_users_path(filter: 'external') do - External - %small.badge= number_with_delimiter(User.external.count) - %li{class: "#{'active' if params[:filter] == "blocked"}"} - = link_to admin_users_path(filter: "blocked") do - Blocked - %small.badge= number_with_delimiter(User.blocked.count) - %li{class: "#{'active' if params[:filter] == "wop"}"} - = link_to admin_users_path(filter: "wop") do - Without projects - %small.badge= number_with_delimiter(User.without_projects.count) + .top-area + .prepend-top-default + = form_tag admin_users_path, method: :get do + - if params[:filter].present? + = hidden_field_tag "filter", h(params[:filter]) + .search-holder + .search-field-holder + = search_field_tag :name, params[:name], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false + = icon("search", class: "search-icon") + .dropdown + - toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end + = dropdown_toggle(toggle_text, { toggle: 'dropdown' }) + %ul.dropdown-menu.dropdown-menu-align-right + %li.dropdown-header + Sort by + %li + = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do + = sort_title_name + = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do + = sort_title_recently_signin + = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do + = sort_title_oldest_signin + = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do + = sort_title_recently_created + = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do + = sort_title_oldest_created + = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do + = sort_title_recently_updated + = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do + = sort_title_oldest_updated + = link_to 'New User', new_admin_user_path, class: 'btn btn-new btn-search' - .row-content-block.second-block - .pull-right - .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_name - %b.caret - %ul.dropdown-menu - %li - = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do - = sort_title_name - = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do - = sort_title_recently_signin - = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do - = sort_title_oldest_signin - = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do - = sort_title_recently_created - = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do - = sort_title_oldest_created - = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do - = sort_title_recently_updated - = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do - = sort_title_oldest_updated + .nav-block + %ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs + .fade-left + = nav_link(html_options: { class: ('active' unless params[:filter]) }) do + = link_to admin_users_path do + Active + %small.badge= number_with_delimiter(User.active.count) + = nav_link(html_options: { class: ('active' if params[:filter] == 'admins') }) do + = link_to admin_users_path(filter: "admins") do + Admins + %small.badge= number_with_delimiter(User.admins.count) + = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_enabled'} filter-two-factor-enabled" }) do + = link_to admin_users_path(filter: 'two_factor_enabled') do + 2FA Enabled + %small.badge= number_with_delimiter(User.with_two_factor.count) + = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_disabled'} filter-two-factor-disabled" }) do + = link_to admin_users_path(filter: 'two_factor_disabled') do + 2FA Disabled + %small.badge= number_with_delimiter(User.without_two_factor.count) + = nav_link(html_options: { class: ('active' if params[:filter] == 'external') }) do + = link_to admin_users_path(filter: 'external') do + External + %small.badge= number_with_delimiter(User.external.count) + = nav_link(html_options: { class: ('active' if params[:filter] == 'blocked') }) do + = link_to admin_users_path(filter: "blocked") do + Blocked + %small.badge= number_with_delimiter(User.blocked.count) + = nav_link(html_options: { class: ('active' if params[:filter] == 'wop') }) do + = link_to admin_users_path(filter: "wop") do + Without projects + %small.badge= number_with_delimiter(User.without_projects.count) + .fade-right - = link_to 'New User', new_admin_user_path, class: "btn btn-new" - = form_tag admin_users_path, method: :get, class: 'form-inline' do - .form-group - = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false - = hidden_field_tag "filter", params[:filter] - = button_tag class: 'btn btn-primary' do - %i.fa.fa-search + %ul.users-list.content-list + - if @users.empty? + %li + .nothing-here-block No users found. + - else + = render partial: 'admin/users/user', collection: @users - - .panel.panel-default - %ul.well-list - - @users.each do |user| - %li - .list-item-name - - if user.blocked? - = icon("lock", class: "cred") - - else - = icon("user", class: "cgreen") - = link_to user.name, [:admin, user] - - if user.admin? - %strong.cred (Admin) - - if user.external? - %strong.cred (External) - - if user == current_user - %span.cred It's you! - .pull-right - %span.light - %i.fa.fa-envelope - = mail_to user.email, user.email, class: 'light' - - .pull-right - = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs' - - unless user == current_user - - if user.ldap_blocked? - = link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do - %i.fa.fa-lock - Unblock - - elsif user.blocked? - = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success' - - else - = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning' - - if user.access_locked? - = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } - - if user.can_be_removed? - = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove' = paginate @users, theme: "gitlab" diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index a3643a00cfe..35c4b862bb7 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,7 +1,7 @@ -- if @note.legacy_diff_note? +- if @note.diff_note? %p.details New comment on diff for - = link_to @note.diff_file_path, @target_url + = link_to @note.diff_file.file_path, @target_url \: = render 'note_message' diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index f1532371b2e..c161ecc3463 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -72,12 +72,11 @@ The diff for this file was not included because it is too large. - else %hr - - diff_commit = diff_file.deleted_file ? @message.diff_refs.first : @message.diff_refs.last - - blob = @message.project.repository.blob_for_diff(diff_commit, diff_file) + - blob = diff_file.blob - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob) %table.code.white - diff_file.highlighted_diff_lines.each do |line| - = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: nil, plain: true} + = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true - else No preview for this file type %br diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml index 434d8644b83..3c6b931f41a 100644 --- a/app/views/projects/_last_push.html.haml +++ b/app/views/projects/_last_push.html.haml @@ -7,7 +7,9 @@ %span You pushed to = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do %strong= event.ref_name - branch + - if @project && event.project != @project + %span at + %strong= link_to_project event.project #{time_ago_with_tooltip(event.created_at)} .pull-right diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index e4f04ca7764..b1c9895f43e 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -4,12 +4,10 @@ %ul.nav-links.no-bottom.js-edit-mode %li.active = link_to '#editor' do - = icon('edit') Edit File %li = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do - = icon('eye') = editing_preview_title(@blob.name) = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index d1c468c4692..4e801cc72fe 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -67,4 +67,4 @@ = render "sidebar" :javascript - new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", "#{@build.status}", "#{trace_with_state[:state]}") + new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", "#{@build.status}", "#{trace_with_state[:state]}") diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml index ae7bb01223e..9d925cacc0d 100644 --- a/app/views/projects/commit/_ci_stage.html.haml +++ b/app/views/projects/commit/_ci_stage.html.haml @@ -7,7 +7,7 @@ = ci_icon_for_status(status) - if stage - = stage.titleize.pluralize + = stage.titleize = render statuses.latest.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true = render statuses.retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true %tr diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 401cb4f7e30..d0da2606587 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -7,8 +7,7 @@ = render "ci_menu" - else %div.block-connector -= render "projects/diffs/diffs", diffs: @diffs, project: @project, - diff_refs: @diff_refs += render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @commit.diff_refs = render "projects/notes/notes_with_form" - if can_collaborate_with_project? - %w(revert cherry-pick).each do |type| diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index f18bc8c41b3..1975287faee 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -2,7 +2,7 @@ - if diff_view == 'parallel' - fluid_layout true -- diff_files = safe_diff_files(diffs, diff_refs) +- diff_files = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository) .content-block.oneline-block.files-changed .inline-parallel-buttons @@ -24,7 +24,7 @@ .files - diff_files.each_with_index do |diff_file, index| - diff_commit = commit_for_diff(diff_file) - - blob = project.repository.blob_for_diff(diff_commit, diff_file) + - blob = diff_file.blob(diff_commit) - next unless blob - blob.load_all_data!(project.repository) unless blob.only_display_raw? diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 2395ea3c275..3b758a1ec4e 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,29 +1,8 @@ -.diff-file.file-holder{id: "diff-#{i}", data: diff_file_html_data(project, diff_commit, diff_file)} +.diff-file.file-holder{id: "diff-#{i}", data: diff_file_html_data(project, diff_file)} .file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"} - - if diff_file.diff.submodule? - %span - = icon('archive fw') - %span - = submodule_link(blob, @commit.id, project.repository) - - else - = blob_icon blob.mode, blob.name - - = link_to "#diff-#{i}" do - - if diff_file.renamed_file - - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) - = old_path - → - = new_path - - else - %span - = diff_file.new_path - - if diff_file.deleted_file - deleted - - - if diff_file.mode_changed? - %small - = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" + = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{i}" + - unless diff_file.submodule? .file-actions.hidden-xs - if blob_text_viewable?(blob) = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file" do @@ -42,17 +21,23 @@ - return unless blob.respond_to?(:text?) - if diff_file.too_large? .nothing-here-block This diff could not be displayed because it is too large. - - elsif blob_text_viewable?(blob) && !project.repository.diffable?(blob) - .nothing-here-block This diff was suppressed by a .gitattributes entry. - - elsif blob_text_viewable?(blob) - - if diff_view == 'parallel' - = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i - - else - = render "projects/diffs/text_file", diff_file: diff_file, index: i - elsif blob.only_display_raw? .nothing-here-block This file is too large to display. + - elsif blob_text_viewable?(blob) + - if !project.repository.diffable?(blob) + .nothing-here-block This diff was suppressed by a .gitattributes entry. + - elsif diff_file.diff_lines.length > 0 + - if diff_view == 'parallel' + = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i + - else + = render "projects/diffs/text_file", diff_file: diff_file, index: i + - else + - if diff_file.mode_changed? + .nothing-here-block File mode changed + - elsif diff_file.renamed_file + .nothing-here-block File moved - elsif blob.image? - - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file) - = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i, diff_refs: diff_refs + - old_blob = diff_file.old_blob(diff_commit) + = render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob, index: i - else .nothing-here-block No preview for this file type diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml new file mode 100644 index 00000000000..95a2772fd0b --- /dev/null +++ b/app/views/projects/diffs/_file_header.html.haml @@ -0,0 +1,25 @@ +- if defined?(blob) && blob && diff_file.submodule? + %span + = icon('archive fw') + %span + = submodule_link(blob, diff_commit.id, project.repository) +- else + = conditional_link_to url.present?, url do + = blob_icon diff_file.b_mode, diff_file.file_path + + - if diff_file.renamed_file + - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) + %strong + = old_path + → + %strong + = new_path + - else + %strong + = diff_file.new_path + - if diff_file.deleted_file + deleted + + - if diff_file.mode_changed? + %small + = "#{diff_file.a_mode} → #{diff_file.b_mode}" diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 2731219ccad..9ec6a7aa5cd 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -1,9 +1,8 @@ - diff = diff_file.diff -- file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path)) +- 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_refs - - old_commit_id = diff_refs.first.id - - old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path)) +- 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 @@ -16,7 +15,7 @@ %div.two-up.view %span.wrap .frame.deleted - %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))} + %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path))} %img{src: old_file_raw_path} %p.image-info.hide %span.meta-filesize= "#{number_to_human_size old_file.size}" @@ -28,7 +27,7 @@ %span.meta-height %span.wrap .frame.added - %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))} + %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path))} %img{src: file_raw_path} %p.image-info.hide %span.meta-filesize= "#{number_to_human_size file.size}" diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index f1577e8a47b..22cad00240a 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -1,3 +1,6 @@ +- plain = local_assigns.fetch(:plain, false) +- line_code = diff_file.line_code(line) +- position = diff_file.position(line) - type = line.type %tr.line_holder{ id: line_code, class: type } - case type @@ -9,18 +12,19 @@ %td.new_line.diff-line-num %td.line_content.match= line.text - else - %td.old_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } + %td.old_line.diff-line-num{ class: type, data: { linenumber: line.old_pos } } - link_text = type == "new" ? " ".html_safe : line.old_pos - - if defined?(plain) && plain + - if plain = link_text - else = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - - if !@diff_notes_disabled && can?(current_user, :create_note, @project) - = link_to_new_diff_note(line_code) + + - if !plain && !@diff_notes_disabled && can?(current_user, :create_note, @project) + = link_to_new_diff_note(line_code, position) %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - link_text = type == "old" ? " ".html_safe : line.new_pos - - if defined?(plain) && plain + - if plain = link_text - else = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - %td.line_content{ class: ['noteable_line', type, line_code], data: { line_code: line_code } }= diff_line_content(line.text, type) + %td.line_content.noteable_line{ class: [type, line_code], data: { line_code: line_code, position: position.to_json } }= diff_line_content(line.text, type) diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 4ecc9528bd2..51f207dce94 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -14,30 +14,28 @@ %td.new_line.diff-line-num %td.line_content.parallel.match= left[:text] - else - %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]} #{'empty-cell' if !left[:number]}"} + %td.old_line.diff-line-num{id: left[:line_code], class: [left[:type], ('empty-cell' unless left[:number])]} = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - if !@diff_notes_disabled && can?(current_user, :create_note, @project) - = link_to_new_diff_note(left[:line_code], 'old') - %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) + = link_to_new_diff_note(left[:line_code], left[:position], 'old') + %td.line_content.parallel.noteable_line{class: [left[:type], left[:line_code], ('empty-cell' if left[:text].empty?)], data: { line_code: left[:line_code], position: left[:position].to_json }}= diff_line_content(left[:text]) - if right[:type] == 'new' - new_line_class = 'new' - new_line_code = right[:line_code] + - new_position = right[:position] - else - new_line_class = nil - new_line_code = left[:line_code] + - new_position = left[:position] - %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }} + %td.new_line.diff-line-num{id: new_line_code, class: [new_line_class, ('empty-cell' unless right[:number])], data: { linenumber: right[:number] }} = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - if !@diff_notes_disabled && can?(current_user, :create_note, @project) - = link_to_new_diff_note(new_line_code, 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) + = link_to_new_diff_note(new_line_code, new_position, 'new') + %td.line_content.parallel.noteable_line{class: [new_line_class, new_line_code, ('empty-cell' if right[:text].empty?)], data: { line_code: new_line_code, position: new_position.to_json }}= diff_line_content(right[:text]) - unless @diff_notes_disabled - notes_left, notes_right = organize_comments(left, right) - if notes_left.present? || notes_right.present? = render "projects/notes/diff_notes_with_reply_parallel", notes_left: notes_left, notes_right: notes_right - -- if diff_file.diff.diff.blank? && diff_file.mode_changed? - .file-mode-changed - File mode changed diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 068593a7dd1..192093d1273 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -4,22 +4,17 @@ %a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show. %table.text-file.code.js-syntax-highlight{ class: too_big ? 'hide' : '' } - - last_line = 0 - - diff_file.highlighted_diff_lines.each_with_index do |line, index| - - line_code = generate_line_code(diff_file.file_path, line) + - diff_file.highlighted_diff_lines.each do |line| - last_line = line.new_pos - = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: line_code} + = render "projects/diffs/line", line: line, diff_file: diff_file - unless @diff_notes_disabled - - diff_notes = @grouped_diff_notes[line_code] + - line_code = diff_file.line_code(line) + - diff_notes = @grouped_diff_notes[line_code] if line_code - if diff_notes = render "projects/notes/diff_notes_with_reply", notes: diff_notes - if last_line > 0 = render "projects/diffs/match_line", { line: "", line_old: last_line, line_new: last_line, bottom: true, new_file: diff_file.new_file } - -- if diff_file.diff.blank? && diff_file.mode_changed? - .file-mode-changed - File mode changed diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 7ce4c1e5555..312bd86ed04 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -6,21 +6,37 @@ - if current_user = auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues") -%div{ class: container_class } - .top-area - = render 'shared/issuable/nav', type: :issues - .nav-controls - - if current_user - = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do - = icon('rss') - %span.icon-label - Subscribe - = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) +%div{ class: (container_class) } + - if @project.issues.any? + .top-area + = render 'shared/issuable/nav', type: :issues + .nav-controls + - if current_user + = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do + = icon('rss') + %span.icon-label + Subscribe + = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) + - if can? current_user, :create_issue, @project + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do + New Issue + = render 'shared/issuable/filter', type: :issues + + .issues-holder + = render "issues" + - else + .blank-state.blank-state-welcome + %h2.blank-state-title.blank-state-welcome-title + Welcome to GitLab Issues + %p.blank-state-text + Code, test, and deploy together + .blank-state + .blank-state-icon + = navbar_icon("issues", size: 50) + %h3.blank-state-title + You don't have any issues right now. + %p.blank-state-text + Issues are the best way to track your project progress - if can? current_user, :create_issue, @project - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do New Issue - - = render 'shared/issuable/filter', type: :issues - - .issues-holder - = render "issues" diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 08a38d283d2..489c632ae22 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -7,7 +7,7 @@ CI build = ci_label_for_status(status) for - - commit = @merge_request.last_commit + - commit = @merge_request.diff_head_commit = succeed "." do = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" %span.ci-coverage @@ -24,7 +24,7 @@ CI build = ci_label_for_status(status) for - - commit = @merge_request.last_commit + - commit = @merge_request.diff_head_commit = succeed "." do = link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace" %span.ci-coverage @@ -33,12 +33,12 @@ .ci_widget = icon("spinner spin") - Checking CI status for #{@merge_request.last_commit_short_sha}… + Checking CI status for #{@merge_request.diff_head_commit.short_id}… .ci_widget.ci-not_found{style: "display:none"} = icon("times-circle") - Could not find CI status for #{@merge_request.last_commit_short_sha}. + Could not find CI status for #{@merge_request.diff_head_commit.short_id}. .ci_widget.ci-error{style: "display:none"} = icon("times-circle") - Could not connect to the CI server. Please check your settings and try again.
\ No newline at end of file + Could not connect to the CI server. Please check your settings and try again. diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 941513febbd..bf2e76f0083 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -2,7 +2,7 @@ = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f| = hidden_field_tag :authenticity_token, form_authenticity_token - = hidden_field_tag :sha, @merge_request.source_sha + = hidden_field_tag :sha, @merge_request.diff_head_sha .accept-merge-holder.clearfix.js-toggle-container .clearfix .accept-action diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index ad898ff153b..2b6b5e05e86 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -16,7 +16,7 @@ - if remove_source_branch_button || user_can_cancel_automatic_merge .clearfix.prepend-top-10 - if remove_source_branch_button - = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.source_sha), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do + = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.diff_head_sha), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do = icon('times') Remove Source Branch When Merged diff --git a/app/views/projects/notes/_diff_notes_with_reply.html.haml b/app/views/projects/notes/_diff_notes_with_reply.html.haml index 8144c1ba49e..ec6c4938efc 100644 --- a/app/views/projects/notes/_diff_notes_with_reply.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply.html.haml @@ -4,5 +4,4 @@ %td.notes_content %ul.notes{ data: { discussion_id: note.discussion_id } } = render partial: "projects/notes/note", collection: notes, as: :note - .discussion-reply-holder - = link_to_reply_discussion(note) + = link_to_reply_discussion(note) diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml index 45986b0d1e8..e50a4f86d03 100644 --- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml @@ -8,8 +8,7 @@ %ul.notes{ data: { discussion_id: note_left.discussion_id } } = render partial: "projects/notes/note", collection: notes_left, as: :note - .discussion-reply-holder - = link_to_reply_discussion(note_left, 'old') + = link_to_reply_discussion(note_left, 'old') - else %td.notes_line.old= "" %td.notes_content.parallel.old= "" @@ -20,8 +19,7 @@ %ul.notes{ data: { discussion_id: note_right.discussion_id } } = render partial: "projects/notes/note", collection: notes_right, as: :note - .discussion-reply-holder - = link_to_reply_discussion(note_right, 'new') + = link_to_reply_discussion(note_right, 'new') - else %td.notes_line.new= "" %td.notes_content.parallel.new= "" diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 03b3f6935d1..7c61ba750fe 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -7,6 +7,7 @@ = f.hidden_field :noteable_id = f.hidden_field :noteable_type = f.hidden_field :type + = f.hidden_field :position = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..." diff --git a/app/views/projects/notes/discussions/_diff_with_notes.html.haml b/app/views/projects/notes/discussions/_diff_with_notes.html.haml index 6401245bf73..4a69b8f8840 100644 --- a/app/views/projects/notes/discussions/_diff_with_notes.html.haml +++ b/app/views/projects/notes/discussions/_diff_with_notes.html.haml @@ -1,30 +1,17 @@ - note = discussion_notes.first -- diff = note.diff -- return unless diff +- diff_file = note.diff_file +- return unless diff_file + +- blob = note.blob + +.diff-file.file-holder + .file-title + = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: note.project, url: diff_note_path(note) -.diff-file - .diff-header - %span - - if diff.deleted_file - = diff.old_path - - else - = diff.new_path - - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode - %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" .diff-content.code.js-syntax-highlight %table - note.truncated_diff_lines.each do |line| - - type = line.type - - line_code = generate_line_code(note.diff_file_path, line) - %tr.line_holder{ id: line_code, class: "#{type}" } - - if type == "match" - %td.old_line.diff-line-num= "..." - %td.new_line.diff-line-num= "..." - %td.line_content.match= line.text - - else - %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? " ".html_safe : line.old_pos } } - %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? " ".html_safe : line.new_pos } } - %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type) + = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true - - if line_code == note.line_code - = render "projects/notes/diff_notes_with_reply", notes: discussion_notes + - if note.for_line?(line) + = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/projects/notes/discussions/_notes.html.haml b/app/views/projects/notes/discussions/_notes.html.haml index e598e3c7c63..a785149549d 100644 --- a/app/views/projects/notes/discussions/_notes.html.haml +++ b/app/views/projects/notes/discussions/_notes.html.haml @@ -3,5 +3,4 @@ .notes{ data: { discussion_id: note.discussion_id } } %ul.notes.timeline = render partial: "projects/notes/note", collection: discussion_notes, as: :note - .discussion-reply-holder - = link_to_reply_discussion(note) + = link_to_reply_discussion(note) diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 28b475d5c2f..6a127afa410 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -50,7 +50,7 @@ - stages.each do |stage| %th.stage %span.has-tooltip{ title: "#{stage.titleize}" } - = stage.titleize.pluralize + = stage.titleize %th Duration %th = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index c375bb6dd1b..368231e73fe 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -7,22 +7,22 @@ .nav-text Tags give the ability to mark specific points in history as being important - - if can? current_user, :push_code, @project - .nav-controls + .nav-controls + - if can? current_user, :push_code, @project = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do New tag - .dropdown.inline - %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} } - %span.light= @sort.humanize - %b.caret - %ul.dropdown-menu.dropdown-menu-align-right - %li - = link_to namespace_project_tags_path(sort: nil) do - Name - = link_to namespace_project_tags_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to namespace_project_tags_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated + .dropdown.inline + %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} } + %span.light= @sort.humanize + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + %li + = link_to namespace_project_tags_path(sort: nil) do + Name + = link_to namespace_project_tags_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to namespace_project_tags_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated .tags - if @tags.any? diff --git a/app/views/shared/icons/_issues.svg b/app/views/shared/icons/_issues.svg deleted file mode 100644 index 2682c27ade9..00000000000 --- a/app/views/shared/icons/_issues.svg +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch --> - <title>Group</title> - <desc>Created with Sketch.</desc> - <defs></defs> - <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <g id="Group" fill="#7E7C7C"> - <path d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2" id="Fill-1"></path> - <path d="M7.1597,4 L8.8887,4 L8.8887,8 L7.1107,8 L7.1597,4 Z M7.1597,9.6667 L8.8887,9.6667 L8.8887,11.4447 L7.1107,11.4447 L7.1597,9.6667 Z" id="Combined-Shape"></path> - </g> - </g> -</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_issues.svg.erb b/app/views/shared/icons/_issues.svg.erb new file mode 100644 index 00000000000..fa8655b5609 --- /dev/null +++ b/app/views/shared/icons/_issues.svg.erb @@ -0,0 +1,4 @@ +<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16" class="gitlab-icon"> + <path fill="#7E7C7C" d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2"></path> + <path fill="#7E7C7C" d="M7.1597,4 L8.8887,4 L8.8887,8 L7.1107,8 L7.1597,4 Z M7.1597,9.6667 L8.8887,9.6667 L8.8887,11.4447 L7.1107,11.4447 L7.1597,9.6667 Z"></path> +</svg> diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index a884e78e6e7..5ae485f36ba 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -3,71 +3,74 @@ - user = member.user %li.js-toggle-container{ class: dom_class(member), id: dom_id(member) } - %span{ class: ("list-item-name" if show_controls) } - - if user - = image_tag avatar_icon(user, 24), class: "avatar s24", alt: '' - %strong - = link_to user.name, user_path(user) - %span.cgray= user.username - - - if user == current_user - %span.label.label-success It's you - - - if user.blocked? - %label.label.label-danger - %strong Blocked - - - if member.request? - %span.cgray - – Requested - = time_ago_with_tooltip(member.requested_at) - - else - = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: '' - %strong= member.invite_email - %span.cgray - – Invited - - if member.created_by - by - = link_to member.created_by.name, user_path(member.created_by) - = time_ago_with_tooltip(member.created_at) - - - if show_controls && can?(current_user, action_member_permission(:admin, member), member.source) - = link_to 'Resend invite', polymorphic_path([:resend_invite, member]), - method: :post, - class: 'btn-xs btn' - - if show_roles - %span.pull-right - %strong= member.human_access + .controls + %strong.control-text= member.human_access - if show_controls + - if !user && can?(current_user, action_member_permission(:admin, member), member.source) + = link_to 'Resend invite', polymorphic_path([:resend_invite, member]), + method: :post, + class: 'btn' + - if can?(current_user, action_member_permission(:update, member), member) = button_tag icon('pencil'), type: 'button', - class: 'btn-xs btn btn-grouped inline js-toggle-button', + class: 'btn inline js-toggle-button', title: 'Edit access level' - if member.request? - = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]), method: :post, - class: 'btn-xs btn btn-success', + class: 'btn btn-success', title: 'Grant access' - if can?(current_user, action_member_permission(:destroy, member), member) - - if current_user == user = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]), method: :delete, data: { confirm: leave_confirmation_message(member.source) }, - class: 'btn-xs btn btn-remove' + class: 'btn btn-remove' - else = link_to icon('trash'), member, remote: true, method: :delete, data: { confirm: remove_member_message(member) }, - class: 'btn-xs btn btn-remove', + class: 'btn btn-remove', title: remove_member_title(member) + + %span{ class: ("list-item-name" if show_controls) } + - if user + = image_tag avatar_icon(user, 40), class: "avatar s40", alt: '' + %strong + = link_to user.name, user_path(user) + %span.cgray= user.username + + - if user == current_user + %span.label.label-success It's you + + - if user.blocked? + %label.label.label-danger + %strong Blocked + + .cgray + - if member.request? + Requested + = time_ago_with_tooltip(member.requested_at) + - else + Joined #{time_ago_with_tooltip(member.created_at)} + + - else + = image_tag avatar_icon(member.invite_email, 40), class: "avatar s40", alt: '' + %strong= member.invite_email + .cgray + Invited + - if member.created_by + by + = link_to member.created_by.name, user_path(member.created_by) + = time_ago_with_tooltip(member.created_at) + + - if show_roles .edit-member.hide.js-toggle-content %br = form_for member, remote: true do |f| diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index 1169bed0382..b7f8551153b 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -1,31 +1,30 @@ - @sort ||= sort_value_recently_updated - personal = params[:personal] - archived = params[:archived] +- namespace_id = params[:namespace_id] .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light - = projects_sort_options_hash[@sort] - %b.caret + - toggle_text = projects_sort_options_hash[@sort] + = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' }) %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable %li.dropdown-header Sort by - projects_sort_options_hash.each do |value, title| %li - = link_to filter_projects_path(sort: value, archived: archived, personal: personal), class: ("is-active" if @sort == value) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: value, archived: archived, personal: personal), class: ("is-active" if @sort == value) do = title %li.divider %li - = link_to filter_projects_path(sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do Hide archived projects %li - = link_to filter_projects_path(sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do Show archived projects - if current_user %li.divider %li - = link_to filter_projects_path(sort: @sort, personal: nil), class: ("is-active" unless personal) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: nil), class: ("is-active" unless personal.present?) do Owned by anyone %li - = link_to filter_projects_path(sort: @sort, personal: true), class: ("is-active" if personal) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true), class: ("is-active" if personal.present?) do Owned by me diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 971f969e25e..8551288e2f2 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -28,18 +28,30 @@ class EmailsOnPushWorker :push end + merge_base_sha = project.merge_base_commit(before_sha, after_sha).try(:sha) + diff_refs = nil compare = nil reverse_compare = false if action == :push compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) - diff_refs = [project.merge_base_commit(before_sha, after_sha), project.commit(after_sha)] + + diff_refs = Gitlab::Diff::DiffRefs.new( + base_sha: merge_base_sha, + start_sha: before_sha, + head_sha: after_sha + ) return false if compare.same if compare.commits.empty? compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha) - diff_refs = [project.merge_base_commit(after_sha, before_sha), project.commit(before_sha)] + + diff_refs = Gitlab::Diff::DiffRefs.new( + base_sha: merge_base_sha, + start_sha: after_sha, + head_sha: before_sha + ) reverse_compare = true diff --git a/db/migrate/20160508202603_add_head_commit_id_to_merge_request_diffs.rb b/db/migrate/20160508202603_add_head_commit_id_to_merge_request_diffs.rb new file mode 100644 index 00000000000..1c4d60e7234 --- /dev/null +++ b/db/migrate/20160508202603_add_head_commit_id_to_merge_request_diffs.rb @@ -0,0 +1,5 @@ +class AddHeadCommitIdToMergeRequestDiffs < ActiveRecord::Migration + def change + add_column :merge_request_diffs, :head_commit_sha, :string + end +end diff --git a/db/migrate/20160508215920_add_positions_to_diff_notes.rb b/db/migrate/20160508215920_add_positions_to_diff_notes.rb new file mode 100644 index 00000000000..2952c25004e --- /dev/null +++ b/db/migrate/20160508215920_add_positions_to_diff_notes.rb @@ -0,0 +1,6 @@ +class AddPositionsToDiffNotes < ActiveRecord::Migration + def change + add_column :notes, :position, :text + add_column :notes, :original_position, :text + end +end diff --git a/db/migrate/20160516224534_add_start_commit_id_to_merge_request_diffs.rb b/db/migrate/20160516224534_add_start_commit_id_to_merge_request_diffs.rb new file mode 100644 index 00000000000..b7fd76ee84b --- /dev/null +++ b/db/migrate/20160516224534_add_start_commit_id_to_merge_request_diffs.rb @@ -0,0 +1,5 @@ +class AddStartCommitIdToMergeRequestDiffs < ActiveRecord::Migration + def change + add_column :merge_request_diffs, :start_commit_sha, :string + end +end diff --git a/db/migrate/20160522215720_add_note_type_and_position_to_sent_notification.rb b/db/migrate/20160522215720_add_note_type_and_position_to_sent_notification.rb new file mode 100644 index 00000000000..4eef16c9408 --- /dev/null +++ b/db/migrate/20160522215720_add_note_type_and_position_to_sent_notification.rb @@ -0,0 +1,22 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddNoteTypeAndPositionToSentNotification < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + add_column :sent_notifications, :note_type, :string + add_column :sent_notifications, :position, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index f6465136e6a..68b9425253c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -593,6 +593,8 @@ ActiveRecord::Schema.define(version: 20160705163108) do t.datetime "updated_at" t.string "base_commit_sha" t.string "real_size" + t.string "head_commit_sha" + t.string "start_commit_sha" end add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree @@ -689,10 +691,12 @@ ActiveRecord::Schema.define(version: 20160705163108) do t.string "line_code" t.string "commit_id" t.integer "noteable_id" - t.boolean "system", default: false, null: false + t.boolean "system", default: false, null: false t.text "st_diff" t.integer "updated_by_id" t.string "type" + t.text "position" + t.text "original_position" end add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree @@ -881,6 +885,8 @@ ActiveRecord::Schema.define(version: 20160705163108) do t.string "commit_id" t.string "reply_key", null: false t.string "line_code" + t.string "note_type" + t.text "position" end add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index 10096779844..7186f707ad6 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -130,27 +130,27 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server first_name: 'givenName' last_name: 'sn' - ## EE only - - # Base where we can search for groups - # - # Ex. ou=groups,dc=gitlab,dc=example - # - group_base: '' - - # The CN of a group containing GitLab administrators - # - # Ex. administrators - # - # Note: Not `cn=administrators` or the full DN - # - admin_group: '' - - # The LDAP attribute containing a user's public SSH key - # - # Ex. ssh_public_key - # - sync_ssh_keys: false + ## EE only + + # Base where we can search for groups + # + # Ex. ou=groups,dc=gitlab,dc=example + # + group_base: '' + + # The CN of a group containing GitLab administrators + # + # Ex. administrators + # + # Note: Not `cn=administrators` or the full DN + # + admin_group: '' + + # The LDAP attribute containing a user's public SSH key + # + # Ex. ssh_public_key + # + sync_ssh_keys: false # GitLab EE only: add more LDAP servers # Choose an ID made of a-z and 0-9 . This ID will be stored in the database diff --git a/features/admin/projects.feature b/features/admin/projects.feature index c5ee80136c8..8929bcf8d80 100644 --- a/features/admin/projects.feature +++ b/features/admin/projects.feature @@ -10,10 +10,11 @@ Feature: Admin Projects Then I should see all non-archived projects And I should not see project "Archive" + @javascript Scenario: I should see all projects in the list Given archived project "Archive" When I visit admin projects page - And I check "Show archived projects" + And I select "Show archived projects" Then I should see all projects And I should see "archived" label @@ -22,6 +23,7 @@ Feature: Admin Projects And I click on first project Then I should see project details + @javascript Scenario: Transfer project Given group 'Web' And I visit admin project page diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index db73309804c..1f4c9020731 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -7,6 +7,7 @@ Feature: Dashboard And project "Shop" has CI enabled And project "Shop" has CI build And project "Shop" has labels: "bug", "feature", "enhancement" + And project "Shop" has issue: "bug report" And I visit dashboard page Scenario: I should see projects list diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 0e97e4d5954..21768c15c17 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -89,13 +89,6 @@ Feature: Project Merge Requests Then The list should be sorted by "Oldest updated" @javascript - Scenario: Visiting Issues after being sorted the list - Given I visit project "Shop" merge requests page - And I sort the list by "Oldest updated" - And I visit project "Shop" issues page - Then The list should be sorted by "Oldest updated" - - @javascript Scenario: Visiting Merge Requests from a differente Project after sorting Given I visit project "Shop" merge requests page And I sort the list by "Oldest updated" diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb index 8613dc537cc..0c89a3db9ad 100644 --- a/features/steps/admin/groups.rb +++ b/features/steps/admin/groups.rb @@ -62,7 +62,8 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps step 'I should see "johndoe@gitlab.com" in team list in every project as "Reporter"' do page.within ".group-users-list" do - expect(page).to have_content "johndoe@gitlab.com – Invited by" + expect(page).to have_content "johndoe@gitlab.com" + expect(page).to have_content "Invited by" expect(page).to have_content "Reporter" end end diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb index a7a28755a6c..d77945a6b9c 100644 --- a/features/steps/admin/projects.rb +++ b/features/steps/admin/projects.rb @@ -18,9 +18,9 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps end end - step 'I check "Show archived projects"' do - page.check 'Show archived projects' - click_button "Search" + step 'I select "Show archived projects"' do + find(:css, '#sort-projects-dropdown').click + click_link 'Show archived projects' end step 'I should see "archived" label' do @@ -45,7 +45,8 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps step 'I transfer project to group \'Web\'' do allow_any_instance_of(Projects::TransferService). to receive(:move_uploads_to_new_namespace).and_return(true) - find(:xpath, "//input[@id='new_namespace_id']").set group.id + click_button 'Search for Namespace' + click_link 'group: web' click_button 'Transfer' end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 640f1720a6c..da848afd48e 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -272,10 +272,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'user "John Doe" leaves a comment like "Line is wrong" on diff' do mr = MergeRequest.find_by(title: "Bug NS-05") - create(:note_on_merge_request_diff, project: project, + create(:diff_note_on_merge_request, project: project, noteable: mr, author: user_exists("John Doe"), - line_code: sample_commit.line_code, note: 'Line is wrong') end @@ -519,7 +518,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step '"Bug NS-05" has CI status' do project = merge_request.source_project project.enable_ci - pipeline = create :ci_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch + pipeline = create :ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch create :ci_build, pipeline: pipeline end diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index e8b1e4b4879..56ef44ec969 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -23,7 +23,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}-true']") do + page.within("form[data-line-code='#{sample_commit.line_code}']") do fill_in "note[note]", with: "Typo, please fix" find(".js-comment-button").trigger("click") sleep 0.05 @@ -33,7 +33,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do click_parallel_diff_line(sample_commit.line_code, 'old') - page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}-true']") do + page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.line_code}']") do fill_in "note[note]", with: "Old comment" find(".js-comment-button").trigger("click") end @@ -41,7 +41,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the right side like "New comment"' do click_parallel_diff_line(sample_commit.line_code, 'new') - page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}-true']") do + page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.line_code}']") do fill_in "note[note]", with: "New comment" find(".js-comment-button").trigger("click") end @@ -51,7 +51,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}-true']") do + page.within("form[data-line-code='#{sample_commit.line_code}']") do fill_in "note[note]", with: "Should fix it :smile:" find('.js-md-preview-button').click end @@ -62,7 +62,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.del_line_code) - page.within("form[id$='#{sample_commit.del_line_code}-true']") do + page.within("form[data-line-code='#{sample_commit.del_line_code}']") do fill_in "note[note]", with: "DRY this up" find('.js-md-preview-button').click end @@ -91,7 +91,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}-true']") do + page.within("form[data-line-code='#{sample_commit.line_code}']") do fill_in 'note[note]', with: ':smile:' click_button('Comment') end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index b3411c03118..0b4920883b8 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -223,6 +223,11 @@ module SharedProject create(:label, project: project, title: 'enhancement') end + step 'project "Shop" has issue: "bug report"' do + project = Project.find_by(name: "Shop") + create(:issue, project: project, title: "bug report") + end + step 'project "Shop" has CI enabled' do project = Project.find_by(name: "Shop") project.enable_ci diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 8cc4368b5c2..db877d2eeb0 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -240,9 +240,9 @@ module API class CommitNote < Grape::Entity expose :note - expose(:path) { |note| note.diff_file_path if note.legacy_diff_note? } - expose(:line) { |note| note.diff_new_line if note.legacy_diff_note? } - expose(:line_type) { |note| note.diff_line_type if note.legacy_diff_note? } + expose(:path) { |note| note.diff_file.try(:file_path) if note.diff_note? } + expose(:line) { |note| note.diff_line.try(:new_line) if note.diff_note? } + expose(:line_type) { |note| note.diff_line.try(:type) if note.diff_note? } expose :author, using: Entities::UserBasic expose :created_at end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 0e94efd4acd..4fcdf8968c9 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -233,8 +233,8 @@ module API render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable? - if params[:sha] && merge_request.source_sha != params[:sha] - render_api_error!("SHA does not match HEAD of source branch: #{merge_request.source_sha}", 409) + if params[:sha] && merge_request.diff_head_sha != params[:sha] + render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409) end merge_params = { diff --git a/lib/gitlab/diff/diff_refs.rb b/lib/gitlab/diff/diff_refs.rb new file mode 100644 index 00000000000..8406ca4269c --- /dev/null +++ b/lib/gitlab/diff/diff_refs.rb @@ -0,0 +1,36 @@ +module Gitlab + module Diff + class DiffRefs + attr_reader :base_sha + attr_reader :start_sha + attr_reader :head_sha + + def initialize(base_sha:, start_sha: base_sha, head_sha:) + @base_sha = base_sha + @start_sha = start_sha + @head_sha = head_sha + end + + def ==(other) + other.is_a?(self.class) && + base_sha == other.base_sha && + start_sha == other.start_sha && + head_sha == other.head_sha + end + + # There is only one case in which we will have `start_sha` and `head_sha`, + # but not `base_sha`, which is when a diff is generated between an + # orphaned branch and another branch, which means there _is_ no base, but + # we're still able to highlight it, and to create diff notes, which are + # the primary things `DiffRefs` are used for. + # `DiffRefs` are "complete" when they have `start_sha` and `head_sha`, + # because `base_sha` can always be derived from this, to return an actual + # sha, or `nil`. + # We have `base_sha` directly available on `DiffRefs` because it's faster# + # than having to look it up in the repo every time. + def complete? + start_sha && head_sha + end + end + end +end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index d2e85cabf72..b0c50edba59 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -1,47 +1,83 @@ module Gitlab module Diff class File - attr_reader :diff, :diff_refs + attr_reader :diff, :repository, :diff_refs delegate :new_file, :deleted_file, :renamed_file, - :old_path, :new_path, to: :diff, prefix: false + :old_path, :new_path, :a_mode, :b_mode, + :submodule?, :too_large?, to: :diff, prefix: false - def initialize(diff, diff_refs) + def initialize(diff, repository:, diff_refs: nil) @diff = diff + @repository = repository @diff_refs = diff_refs end + def position(line) + return unless diff_refs + + Position.new( + old_path: old_path, + new_path: new_path, + old_line: line.old_line, + new_line: line.new_line, + diff_refs: diff_refs + ) + end + + def line_code(line) + return if line.meta? + + Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) + end + + def line_for_line_code(code) + diff_lines.find { |line| line_code(line) == code } + end + + def line_for_position(pos) + diff_lines.find { |line| position(line) == pos } + end + + def position_for_line_code(code) + line = line_for_line_code(code) + position(line) if line + end + + def line_code_for_position(pos) + line = line_for_position(pos) + line_code(line) if line + end + + def content_commit + return unless diff_refs + + repository.commit(deleted_file ? old_ref : new_ref) + end + def old_ref - diff_refs[0] if diff_refs + diff_refs.try(:base_sha) end def new_ref - diff_refs[1] if diff_refs + diff_refs.try(:head_sha) end - # Array of Gitlab::DIff::Line objects + # Array of Gitlab::Diff::Line objects def diff_lines - @lines ||= parser.parse(raw_diff.each_line).to_a - end - - def too_large? - diff.too_large? + @lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a end def highlighted_diff_lines - Gitlab::Diff::Highlight.new(self).highlight + @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight end def parallel_diff_lines - Gitlab::Diff::ParallelDiff.new(self).parallelize + @parallel_diff_lines ||= Gitlab::Diff::ParallelDiff.new(self).parallelize end def mode_changed? - !!(diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode) - end - - def parser - Gitlab::Diff::Parser.new + a_mode && b_mode && a_mode != b_mode end def raw_diff @@ -53,17 +89,15 @@ module Gitlab end def prev_line(index) - if index > 0 - diff_lines[index - 1] - end + diff_lines[index - 1] if index > 0 + end + + def paths + [old_path, new_path].compact end def file_path - if diff.new_path.present? - diff.new_path - elsif diff.old_path.present? - diff.old_path - end + new_path.presence || old_path end def added_lines @@ -73,6 +107,21 @@ module Gitlab def removed_lines diff_lines.count(&:removed?) end + + def old_blob(commit = content_commit) + return unless commit + + parent_id = commit.parent_id + return unless parent_id + + repository.blob_at(parent_id, old_path) + end + + def blob(commit = content_commit) + return unless commit + + repository.blob_at(commit.id, file_path) + end end end end diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 9429b3ff88d..649a265a02c 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -1,11 +1,13 @@ module Gitlab module Diff class Highlight - attr_reader :diff_file, :diff_lines, :raw_lines + attr_reader :diff_file, :diff_lines, :raw_lines, :repository delegate :old_path, :new_path, :old_ref, :new_ref, to: :diff_file, prefix: :diff - def initialize(diff_lines) + def initialize(diff_lines, repository: nil) + @repository = repository + if diff_lines.is_a?(Gitlab::Diff::File) @diff_file = diff_lines @diff_lines = @diff_file.diff_lines @@ -19,7 +21,7 @@ module Gitlab @diff_lines.map.with_index do |diff_line, i| diff_line = diff_line.dup # ignore highlighting for "match" lines - next diff_line if diff_line.type == 'match' || diff_line.type == 'nonewline' + next diff_line if diff_line.meta? rich_line = highlight_line(diff_line) || diff_line.text @@ -40,12 +42,12 @@ module Gitlab line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' ' - case diff_line.type - when 'new', nil - rich_line = new_lines[diff_line.new_pos - 1] - when 'old' - rich_line = old_lines[diff_line.old_pos - 1] - end + rich_line = + if diff_line.unchanged? || diff_line.added? + new_lines[diff_line.new_pos - 1] + elsif diff_line.removed? + old_lines[diff_line.old_pos - 1] + end # Only update text if line is found. This will prevent # issues with submodules given the line only exists in diff content. @@ -58,19 +60,12 @@ module Gitlab def old_lines return unless diff_file - @old_lines ||= Gitlab::Highlight.highlight_lines(*processing_args(:old)) + @old_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_old_ref, diff_old_path) end def new_lines return unless diff_file - @new_lines ||= Gitlab::Highlight.highlight_lines(*processing_args(:new)) - end - - def processing_args(version) - ref = send("diff_#{version}_ref") - path = send("diff_#{version}_path") - - [ref.project.repository, ref.id, path] + @new_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_new_ref, diff_new_path) end end end diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb index 03730b435ad..c6189d660c2 100644 --- a/lib/gitlab/diff/line.rb +++ b/lib/gitlab/diff/line.rb @@ -9,6 +9,18 @@ module Gitlab @old_pos, @new_pos = old_pos, new_pos end + def old_line + old_pos unless added? || meta? + end + + def new_line + new_pos unless removed? || meta? + end + + def unchanged? + type.nil? + end + def added? type == 'new' end @@ -16,6 +28,10 @@ module Gitlab def removed? type == 'old' end + + def meta? + type == 'match' || type == 'nonewline' + end end end end diff --git a/lib/gitlab/diff/line_mapper.rb b/lib/gitlab/diff/line_mapper.rb new file mode 100644 index 00000000000..576a761423e --- /dev/null +++ b/lib/gitlab/diff/line_mapper.rb @@ -0,0 +1,64 @@ +# When provided a diff for a specific file, maps old line numbers to new line +# numbers and back, to find out where a specific line in a file was moved by the +# changes. +module Gitlab + module Diff + class LineMapper + attr_accessor :diff_file + + def initialize(diff_file) + @diff_file = diff_file + end + + # Find new line number for old line number. + def old_to_new(old_line) + map_line_number(old_line, from: :old_line, to: :new_line) + end + + # Find old line number for new line number. + def new_to_old(new_line) + map_line_number(new_line, from: :new_line, to: :old_line) + end + + private + + def diff_lines + @diff_lines ||= @diff_file.diff_lines + end + + # Find old/new line number based on its old/new counterpart line number. + def map_line_number(from_line, from:, to:) + # If no diff file could be found, the file wasn't changed, and the + # mapped line number is the same as the specified line number. + return from_line unless diff_file + + # To find the mapped line number for the specified line number, + # we need to find: + # - The diff line with that exact line number, if it is in the diff context + # - The first diff line with a higher line number, if it falls between diff contexts + # - The last known diff line, if it falls after the last diff context + diff_line = diff_lines.find do |diff_line| + diff_from_line = diff_line.send(from) + diff_from_line && diff_from_line >= from_line + end + diff_line ||= diff_lines.last + + # If no diff line could be found, the file wasn't changed, and the + # mapped line number is the same as the specified line number. + return from_line unless diff_line + + diff_from_line = diff_line.send(from) + diff_to_line = diff_line.send(to) + + # If the line was removed, there is no mapped line number. + return unless diff_to_line + + # Because we may not have the diff line with the exact line number + # we were looking for, we need to adjust the mapped line number. + distance = diff_from_line - from_line + + diff_to_line - distance + end + end + end +end diff --git a/lib/gitlab/diff/parallel_diff.rb b/lib/gitlab/diff/parallel_diff.rb index 74f9b3c050a..1c1fc148123 100644 --- a/lib/gitlab/diff/parallel_diff.rb +++ b/lib/gitlab/diff/parallel_diff.rb @@ -15,17 +15,19 @@ module Gitlab highlighted_diff_lines.each do |line| full_line = line.text type = line.type - line_code = generate_line_code(diff_file.file_path, line) + line_code = diff_file.line_code(line) line_new = line.new_pos line_old = line.old_pos + position = diff_file.position(line) next_line = diff_file.next_line(line.index) if next_line next_line = highlighted_diff_lines[next_line.index] - next_line_code = generate_line_code(diff_file.file_path, next_line) + full_next_line = next_line.text + next_line_code = diff_file.line_code(next_line) next_type = next_line.type - next_line = next_line.text + next_position = diff_file.position(next_line) end case type @@ -37,12 +39,14 @@ module Gitlab number: line_old, text: full_line, line_code: line_code, + position: position }, right: { type: type, number: line_new, text: full_line, - line_code: line_code + line_code: line_code, + position: position } } when 'old' @@ -55,12 +59,14 @@ module Gitlab number: line_old, text: full_line, line_code: line_code, + position: position }, right: { type: next_type, number: line_new, - text: next_line, - line_code: next_line_code + text: full_next_line, + line_code: next_line_code, + position: next_position, } } skip_next = true @@ -73,12 +79,14 @@ module Gitlab number: line_old, text: full_line, line_code: line_code, + position: position }, right: { type: next_type, number: nil, text: "", - line_code: nil + line_code: nil, + position: nil } } end @@ -95,12 +103,14 @@ module Gitlab number: nil, text: "", line_code: line_code, + position: position }, right: { type: type, number: line_new, text: full_line, - line_code: line_code + line_code: line_code, + position: position } } end @@ -108,12 +118,6 @@ module Gitlab end lines end - - private - - def generate_line_code(file_path, line) - Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) - end end end end diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb new file mode 100644 index 00000000000..989fff8918e --- /dev/null +++ b/lib/gitlab/diff/position.rb @@ -0,0 +1,155 @@ +# Defines a specific location, identified by paths and line numbers, +# within a specific diff, identified by start, head and base commit ids. +module Gitlab + module Diff + class Position + attr_reader :old_path + attr_reader :new_path + attr_reader :old_line + attr_reader :new_line + attr_reader :base_sha + attr_reader :start_sha + attr_reader :head_sha + + def initialize(attrs = {}) + @old_path = attrs[:old_path] + @new_path = attrs[:new_path] + @old_line = attrs[:old_line] + @new_line = attrs[:new_line] + + if attrs[:diff_refs] + @base_sha = attrs[:diff_refs].base_sha + @start_sha = attrs[:diff_refs].start_sha + @head_sha = attrs[:diff_refs].head_sha + else + @base_sha = attrs[:base_sha] + @start_sha = attrs[:start_sha] + @head_sha = attrs[:head_sha] + end + end + + # `Gitlab::Diff::Position` objects are stored as serialized attributes in + # `DiffNote`, which use YAML to encode and decode objects. + # `#init_with` and `#encode_with` can be used to customize the en/decoding + # behavior. In this case, we override these to prevent memoized instance + # variables like `@diff_file` and `@diff_line` from being serialized. + def init_with(coder) + initialize(coder['attributes']) + + self + end + + def encode_with(coder) + coder['attributes'] = self.to_h + end + + def key + @key ||= [base_sha, start_sha, head_sha, Digest::SHA1.hexdigest(old_path || ""), Digest::SHA1.hexdigest(new_path || ""), old_line, new_line] + end + + def ==(other) + other.is_a?(self.class) && key == other.key + end + + def to_h + { + old_path: old_path, + new_path: new_path, + old_line: old_line, + new_line: new_line, + base_sha: base_sha, + start_sha: start_sha, + head_sha: head_sha + } + end + + def inspect + %(#<#{self.class}:#{object_id} #{to_h}>) + end + + def complete? + file_path.present? && + (old_line || new_line) && + diff_refs.complete? + end + + def to_json + JSON.generate(self.to_h) + end + + def type + if old_line && new_line + nil + elsif new_line + 'new' + else + 'old' + end + end + + def unchanged? + type.nil? + end + + def added? + type == 'new' + end + + def removed? + type == 'old' + end + + def paths + [old_path, new_path].compact.uniq + end + + def file_path + new_path.presence || old_path + end + + def diff_refs + @diff_refs ||= DiffRefs.new(base_sha: base_sha, start_sha: start_sha, head_sha: head_sha) + end + + def diff_file(repository) + @diff_file ||= begin + if RequestStore.active? + key = { + project_id: repository.project.id, + start_sha: start_sha, + head_sha: head_sha, + path: file_path + } + + RequestStore.fetch(key) { find_diff_file(repository) } + else + find_diff_file(repository) + end + end + end + + def diff_line(repository) + @diff_line ||= diff_file(repository).line_for_position(self) + end + + def line_code(repository) + @line_code ||= diff_file(repository).line_code_for_position(self) + end + + private + + def find_diff_file(repository) + diffs = Gitlab::Git::Compare.new( + repository.raw_repository, + start_sha, + head_sha + ).diffs(paths: paths) + + diff = diffs.first + return unless diff + + Gitlab::Diff::File.new(diff, repository: repository, diff_refs: diff_refs) + end + end + end +end diff --git a/lib/gitlab/diff/position_tracer.rb b/lib/gitlab/diff/position_tracer.rb new file mode 100644 index 00000000000..4d04f867268 --- /dev/null +++ b/lib/gitlab/diff/position_tracer.rb @@ -0,0 +1,168 @@ +# Finds the diff position in the new diff that corresponds to the same location +# specified by the provided position in the old diff. +module Gitlab + module Diff + class PositionTracer + attr_accessor :repository + attr_accessor :old_diff_refs + attr_accessor :new_diff_refs + attr_accessor :paths + + def initialize(repository:, old_diff_refs:, new_diff_refs:, paths: nil) + @repository = repository + @old_diff_refs = old_diff_refs + @new_diff_refs = new_diff_refs + @paths = paths + end + + def trace(old_position) + return unless old_diff_refs.complete? && new_diff_refs.complete? + return unless old_position.diff_refs == old_diff_refs + + # Suppose we have an MR with source branch `feature` and target branch `master`. + # When the MR was created, the head of `master` was commit A, and the + # head of `feature` was commit B, resulting in the original diff A->B. + # Since creation, `master` was updated to C. + # Now `feature` is being updated to D, and the newly generated MR diff is C->D. + # It is possible that C and D are direct decendants of A and B respectively, + # but this isn't necessarily the case as rebases and merges come into play. + # + # Suppose we have a diff note on the original diff A->B. Now that the MR + # is updated, we need to find out what line in C->D corresponds to the + # line the note was originally created on, so that we can update the diff note's + # records and continue to display it in the right place in the diffs. + # If we cannot find this line in the new diff, this means the diff note is now + # outdated, and we will display that fact to the user. + # + # In the new diff, the file the diff note was originally created on may + # have been renamed, deleted or even created, if the file existed in A and B, + # but was removed in C, and restored in D. + # + # Every diff note stores a Position object that defines a specific location, + # identified by paths and line numbers, within a specific diff, identified + # by start, head and base commit ids. + # + # For diff notes for diff A->B, the position looks like this: + # Position + # base_sha - ID of commit A + # head_sha - ID of commit B + # old_path - path as of A (nil if file was newly created) + # new_path - path as of B (nil if file was deleted) + # old_line - line number as of A (nil if file was newly created) + # new_line - line number as of B (nil if file was deleted) + # + # We can easily update `base_sha` and `head_sha` to hold the IDs of commits C and D, + # but need to find the paths and line numbers as of C and D. + # + # If the file was unchanged or newly created in A->B, the path as of D can be found + # by generating diff B->D ("head to head"), finding the diff file with + # `diff_file.old_path == position.new_path`, and taking `diff_file.new_path`. + # The path as of C can be found by taking diff C->D, finding the diff file + # with that same `new_path` and taking `diff_file.old_path`. + # The line number as of D can be found by using the LineMapper on diff B->D + # and providing the line number as of B. + # The line number as of C can be found by using the LineMapper on diff C->D + # and providing the line number as of D. + # + # If the file was deleted in A->B, the path as of C can be found + # by generating diff A->C ("base to base"), finding the diff file with + # `diff_file.old_path == position.old_path`, and taking `diff_file.new_path`. + # The path as of D can be found by taking diff C->D, finding the diff file + # with that same `old_path` and taking `diff_file.new_path`. + # The line number as of C can be found by using the LineMapper on diff A->C + # and providing the line number as of A. + # The line number as of D can be found by using the LineMapper on diff C->D + # and providing the line number as of C. + + results = nil + results ||= trace_added_line(old_position) if old_position.added? || old_position.unchanged? + results ||= trace_removed_line(old_position) if old_position.removed? || old_position.unchanged? + + return unless results + + file_diff, old_line, new_line = results + + Position.new( + old_path: file_diff.old_path, + new_path: file_diff.new_path, + head_sha: new_diff_refs.head_sha, + start_sha: new_diff_refs.start_sha, + base_sha: new_diff_refs.base_sha, + old_line: old_line, + new_line: new_line + ) + end + + private + + def trace_added_line(old_position) + file_path = old_position.new_path + + return unless diff_head_to_head + + file_head_to_head = diff_head_to_head.find { |diff_file| diff_file.old_path == file_path } + + file_path = file_head_to_head.new_path if file_head_to_head + + new_line = LineMapper.new(file_head_to_head).old_to_new(old_position.new_line) + + return unless new_line + + file_diff = new_diffs.find { |diff_file| diff_file.new_path == file_path } + return unless file_diff + + old_line = LineMapper.new(file_diff).new_to_old(new_line) + + [file_diff, old_line, new_line] + end + + def trace_removed_line(old_position) + file_path = old_position.old_path + + return unless diff_base_to_base + + file_base_to_base = diff_base_to_base.find { |diff_file| diff_file.old_path == file_path } + + file_path = file_base_to_base.old_path if file_base_to_base + + old_line = LineMapper.new(file_base_to_base).old_to_new(old_position.old_line) + + return unless old_line + + file_diff = new_diffs.find { |diff_file| diff_file.old_path == file_path } + return unless file_diff + + new_line = LineMapper.new(file_diff).old_to_new(old_line) + + [file_diff, old_line, new_line] + end + + def diff_base_to_base + @diff_base_to_base ||= diff_files(old_diff_refs.base_sha || old_diff_refs.start_sha, new_diff_refs.base_sha || new_diff_refs.start_sha) + end + + def diff_head_to_head + @diff_head_to_head ||= diff_files(old_diff_refs.head_sha, new_diff_refs.head_sha) + end + + def new_diffs + @new_diffs ||= diff_files(new_diff_refs.start_sha, new_diff_refs.head_sha, use_base: true) + end + + def diff_files(start_sha, head_sha, use_base: false) + base_sha = self.repository.merge_base(start_sha, head_sha) || start_sha + + diffs = self.repository.raw_repository.diff( + use_base ? base_sha : start_sha, + head_sha, + {}, + *paths + ) + + diffs.decorate! do |diff| + Gitlab::Diff::File.new(diff, repository: self.repository) + end + end + end + end +end diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 047c77c6fc2..97701b0cd42 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -33,11 +33,15 @@ module Gitlab end def commits - @commits ||= (Commit.decorate(compare.commits, project) if compare) + return unless compare + + @commits ||= Commit.decorate(compare.commits, project) end def diffs - @diffs ||= (safe_diff_files(compare.diffs(max_files: 30), diff_refs) if compare) + return unless compare + + @diffs ||= safe_diff_files(compare.diffs(max_files: 30), diff_refs: diff_refs, repository: project.repository) end def diffs_count diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index 97ef9851d71..1c671a7487b 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -104,15 +104,7 @@ module Gitlab end def create_note(reply) - Notes::CreateService.new( - sent_notification.project, - sent_notification.recipient, - note: reply, - noteable_type: sent_notification.noteable_type, - noteable_id: sent_notification.noteable_id, - commit_id: sent_notification.commit_id, - line_code: sent_notification.line_code - ).execute + sent_notification.create_note(reply) end end end diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb index a15fc84b418..7d2d545b84e 100644 --- a/lib/gitlab/github_import/branch_formatter.rb +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -4,7 +4,7 @@ module Gitlab delegate :repo, :sha, :ref, to: :raw_data def exists? - project.repository.branch_exists?(ref) + branch_exists? && commit_exists? end def name @@ -15,11 +15,15 @@ module Gitlab repo.present? end - def valid? - repo.present? + private + + def branch_exists? + project.repository.branch_exists?(ref) end - private + def commit_exists? + project.repository.commit(sha).present? + end def short_id sha.to_s[0..7] diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 730978d502b..3932fcb1eda 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -131,8 +131,10 @@ module Gitlab def clean_up_restored_branches(branches) branches.each do |name, _| client.delete_ref(repo, "heads/#{name}") - project.repository.rm_branch(project.creator, name) + project.repository.delete_branch(name) rescue Rugged::ReferenceError end + + project.repository.after_remove_branch end def apply_labels(issuable) diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 498b00cb658..a4ea2210abd 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -11,10 +11,10 @@ module Gitlab description: description, source_project: source_branch_project, source_branch: source_branch_name, - head_source_sha: source_branch_sha, + source_branch_sha: source_branch_sha, target_project: target_branch_project, target_branch: target_branch_name, - base_target_sha: target_branch_sha, + target_branch_sha: target_branch_sha, state: state, milestone: milestone, author_id: author_id, diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 62c736451b7..2249904145c 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -29,7 +29,7 @@ module Gitlab def execute(cmd) output, status = Gitlab::Popen.popen(cmd) - @shared.error(output.to_s) unless status.zero? + @shared.error(Gitlab::ImportExport::Error.new(output.to_s)) unless status.zero? status.zero? end diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb index dd4fdf37309..6a60b65071f 100644 --- a/lib/gitlab/import_export/saver.rb +++ b/lib/gitlab/import_export/saver.rb @@ -17,7 +17,7 @@ module Gitlab Rails.logger.info("Saved project export #{archive_file}") archive_file else - @shared.error("Unable to save #{archive_file} into #{@shared.export_path}") + @shared.error(Gitlab::ImportExport::Error.new("Unable to save #{archive_file} into #{@shared.export_path}")) false end rescue => e diff --git a/lib/gitlab/timeless.rb b/lib/gitlab/timeless.rb new file mode 100644 index 00000000000..b290c716f97 --- /dev/null +++ b/lib/gitlab/timeless.rb @@ -0,0 +1,16 @@ +module Gitlab + module Timeless + def self.timeless(model, &block) + original_record_timestamps = model.record_timestamps + model.record_timestamps = false + + if block.arity.abs == 1 + block.call(model) + else + block.call + end + ensure + model.record_timestamps = original_record_timestamps + end + end +end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index ef1241f8600..bc0193a6c32 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -38,12 +38,10 @@ module Gitlab end def send_git_diff(repository, diff_refs) - from, to = diff_refs - params = { 'RepoPath' => repository.path_to_repo, - 'ShaFrom' => from.sha, - 'ShaTo' => to.sha + 'ShaFrom' => diff_refs.start_sha, + 'ShaTo' => diff_refs.head_sha } [ @@ -52,11 +50,11 @@ module Gitlab ] end - def send_git_patch(repository, from, to) + def send_git_patch(repository, diff_refs) params = { 'RepoPath' => repository.path_to_repo, - 'ShaFrom' => from, - 'ShaTo' => to + 'ShaFrom' => diff_refs.start_sha, + 'ShaTo' => diff_refs.head_sha } [ diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb index 4cb8b8da150..8eaacef2024 100644 --- a/spec/controllers/admin/projects_controller_spec.rb +++ b/spec/controllers/admin/projects_controller_spec.rb @@ -11,12 +11,12 @@ describe Admin::ProjectsController do render_views it 'retrieves the project for the given visibility level' do - get :index, visibility_levels: [Gitlab::VisibilityLevel::PUBLIC] + get :index, visibility_level: [Gitlab::VisibilityLevel::PUBLIC] expect(response.body).to match(project.name) end it 'does not retrieve the project' do - get :index, visibility_levels: [Gitlab::VisibilityLevel::INTERNAL] + get :index, visibility_level: [Gitlab::VisibilityLevel::INTERNAL] expect(response.body).not_to match(project.name) end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index eff74e12869..c4b57e77804 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -103,7 +103,7 @@ describe Projects::MergeRequestsController do id: merge_request.iid, format: :patch) - expect(response.headers['Gitlab-Workhorse-Send-Data']).to start_with("git-format-patch:") + expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-format-patch:") end end end @@ -212,7 +212,7 @@ describe Projects::MergeRequestsController do context 'when the sha parameter matches the source SHA' do def merge_with_sha - post :merge, base_params.merge(sha: merge_request.source_sha) + post :merge, base_params.merge(sha: merge_request.diff_head_sha) end it 'returns :success' do @@ -229,11 +229,11 @@ describe Projects::MergeRequestsController do context 'when merge_when_build_succeeds is passed' do def merge_when_build_succeeds - post :merge, base_params.merge(sha: merge_request.source_sha, merge_when_build_succeeds: '1') + post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_build_succeeds: '1') end before do - create(:ci_empty_pipeline, project: project, sha: merge_request.source_sha, ref: merge_request.source_branch) + create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) end it 'returns :merge_when_build_succeeds' do diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 696cf276e57..83e38095feb 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -10,21 +10,46 @@ FactoryGirl.define do on_issue factory :note_on_commit, traits: [:on_commit] - factory :note_on_commit_diff, traits: [:on_commit, :on_diff], class: LegacyDiffNote factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note] factory :note_on_merge_request, traits: [:on_merge_request] - factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff], class: LegacyDiffNote factory :note_on_project_snippet, traits: [:on_project_snippet] factory :system_note, traits: [:system] + factory :legacy_diff_note_on_commit, traits: [:on_commit, :legacy_diff_note], class: LegacyDiffNote + factory :legacy_diff_note_on_merge_request, traits: [:on_merge_request, :legacy_diff_note], class: LegacyDiffNote + + factory :diff_note_on_merge_request, traits: [:on_merge_request], class: DiffNote do + position do + Gitlab::Diff::Position.new( + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 14, + diff_refs: noteable.diff_refs + ) + end + end + + factory :diff_note_on_commit, traits: [:on_commit], class: DiffNote do + position do + Gitlab::Diff::Position.new( + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 14, + diff_refs: project.commit(commit_id).try(:diff_refs) + ) + end + end + trait :on_commit do noteable nil - noteable_id nil noteable_type 'Commit' + noteable_id nil commit_id RepoHelpers.sample_commit.id end - trait :on_diff do + trait :legacy_diff_note do line_code "0_184_184" end diff --git a/spec/factories/notification_settings.rb b/spec/factories/notification_settings.rb new file mode 100644 index 00000000000..b5e96d18b8f --- /dev/null +++ b/spec/factories/notification_settings.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :notification_setting do + source factory: :empty_project + user + level 3 + events [] + end +end diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 006a06b8235..4b9b5394b61 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -7,6 +7,7 @@ describe 'Filter issues', feature: true do let!(:user) { create(:user)} let!(:milestone) { create(:milestone, project: project) } let!(:label) { create(:label, project: project) } + let!(:issue1) { create(:issue, project: project) } before do project.team << [user, :master] @@ -196,6 +197,7 @@ describe 'Filter issues', feature: true do page.within '.labels-filter' do click_link 'bug' end + find('.dropdown-menu-close-icon').click page.within '.issues-list' do expect(page).to have_selector('.issue', count: 1) @@ -287,7 +289,7 @@ describe 'Filter issues', feature: true do wait_for_ajax page.within '.issues-list' do - expect(first('.issue')).to have_content('Frontend') + expect(page).to have_content('Frontend') end end end diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index b4d2201c729..f676200ecf3 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -30,7 +30,7 @@ feature 'Merge request created from fork' do given(:pipeline) do create(:ci_pipeline_with_two_job, project: fork_project, - sha: merge_request.last_commit.id, + sha: merge_request.diff_head_sha, ref: merge_request.source_branch) end diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index c5e6412d7bf..96f7b8c9932 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -12,7 +12,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do end context "Active build for Merge Request" do - let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) } let!(:ci_build) { create(:ci_build, pipeline: pipeline) } before do @@ -47,7 +47,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do merge_user: user, title: "MepMep", merge_when_build_succeeds: true) end - let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) } let!(:ci_build) { create(:ci_build, pipeline: pipeline) } before do diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb index 65e9185ec24..80e8b8fc642 100644 --- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb @@ -19,7 +19,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: end context 'when project has CI enabled' do - let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) } context 'when merge requests can only be merged if the build succeeds' do before do diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 737efcef45d..5174168713c 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -166,12 +166,14 @@ describe 'Comments', feature: true do click_diff_line is_expected. - to have_css("tr[id='#{line_code}'] + .js-temp-notes-holder form", + to have_css("form[data-line-code='#{line_code}']", count: 1) end it 'should be removed when canceled' do - page.within(".diff-file form[id$='#{line_code}-true']") do + is_expected.to have_css('.js-temp-notes-holder') + + page.within("form[data-line-code='#{line_code}']") do find('.js-close-discussion-note-form').trigger('click') end diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb index 98703ef3ac4..e7ee0aaea3c 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/pipelines_spec.rb @@ -123,7 +123,7 @@ describe "Pipelines" do before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } it 'showing a list of builds' do - expect(page).to have_content('Tests') + expect(page).to have_content('Test') expect(page).to have_content(@success.id) expect(page).to have_content('Deploy') expect(page).to have_content(@failed.id) diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 9d66f76ef58..bc3bf53fe9d 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -42,6 +42,23 @@ feature 'project import', feature: true, js: true do expect(project.import_status).to eq('finished') end + scenario 'invalid project' do + project = create(:project, namespace_id: 2) + + visit new_project_path + + select2('2', from: '#project_namespace_id') + fill_in :project_path, with: project.name, visible: true + click_link 'GitLab export' + + attach_file('file', file) + click_on 'Import project' + + page.within('.flash-container') do + expect(page).to have_content('Project could not be imported') + end + end + def wiki_exists? wiki = ProjectWiki.new(project) File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty? diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 85923f0a19d..d0a301038c4 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe "Search", feature: true do let(:user) { create(:user) } let(:project) { create(:project, namespace: user.namespace) } + let!(:issue) { create(:issue, project: project, assignee: user) } + let!(:issue2) { create(:issue, project: project, author: user) } before do login_with(user) diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml index a8b7907d4ba..7d01183e3ef 100644 --- a/spec/fixtures/parallel_diff_result.yml +++ b/spec/fixtures/parallel_diff_result.yml @@ -3,322 +3,818 @@ :type: match :number: 6 :text: "@@ -6,12 +6,18 @@ module Popen" - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 + :line_code: + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: match :number: 6 :text: "@@ -6,12 +6,18 @@ module Popen" - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 + :line_code: + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 6 :text: |2 <span id="LC6" class="line"></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 6 + :new_line: 6 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 6 :text: |2 <span id="LC6" class="line"></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 6 + :new_line: 6 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 7 :text: |2 <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 7 + :new_line: 7 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 7 :text: |2 <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 7 + :new_line: 7 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 8 :text: |2 <span id="LC8" class="line"> <span class="k">unless</span> <span class="n">cmd</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 8 + :new_line: 8 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 8 :text: |2 <span id="LC8" class="line"> <span class="k">unless</span> <span class="n">cmd</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 8 + :new_line: 8 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: old :number: 9 :text: | -<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 9 + :new_line: + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 9 :text: | +<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 9 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 10 :text: |2 <span id="LC10" class="line"> <span class="k">end</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 10 + :new_line: 10 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 10 :text: |2 <span id="LC10" class="line"> <span class="k">end</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 10 + :new_line: 10 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 11 :text: |2 <span id="LC11" class="line"></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 11 + :new_line: 11 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 11 :text: |2 <span id="LC11" class="line"></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 11 + :new_line: 11 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 12 :text: |2 <span id="LC12" class="line"> <span class="n">path</span> <span class="o">||=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 12 + :new_line: 12 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 12 :text: |2 <span id="LC12" class="line"> <span class="n">path</span> <span class="o">||=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 12 + :new_line: 12 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: old :number: 13 :text: | -<span id="LC13" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"PWD"</span> <span class="o">=></span> <span class="n">path</span> <span class="p">}</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 13 + :new_line: + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: old :number: :text: '' :line_code: + :position: - :left: :type: old :number: 14 :text: | -<span id="LC14" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">chdir: </span><span class="n">path</span> <span class="p">}</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 14 + :new_line: + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 13 :text: | +<span id="LC13" class="line"></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 13 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 14 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 14 :text: | +<span id="LC14" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 14 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 15 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 15 :text: | +<span id="LC15" class="line"> <span class="s2">"PWD"</span> <span class="o">=></span> <span class="n">path</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 15 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 16 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 16 :text: | +<span id="LC16" class="line"> <span class="p">}</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 16 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 17 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 17 :text: | +<span id="LC17" class="line"></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 17 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 18 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 18 :text: | +<span id="LC18" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 18 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 19 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 19 :text: | +<span id="LC19" class="line"> <span class="ss">chdir: </span><span class="n">path</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 19 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 20 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 20 :text: | +<span id="LC20" class="line"> <span class="p">}</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 20 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 15 :text: |2 <span id="LC21" class="line"></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 15 + :new_line: 21 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 21 :text: |2 <span id="LC21" class="line"></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 15 + :new_line: 21 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 16 :text: |2 <span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 16 + :new_line: 22 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 22 :text: |2 <span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 16 + :new_line: 22 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 17 :text: |2 <span id="LC23" class="line"> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 17 + :new_line: 23 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 23 :text: |2 <span id="LC23" class="line"> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 17 + :new_line: 23 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: match :number: 19 :text: "@@ -19,6 +25,7 @@ module Popen" - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 + :line_code: + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: match :number: 25 :text: "@@ -19,6 +25,7 @@ module Popen" - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 + :line_code: + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 19 :text: |2 <span id="LC25" class="line"></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 19 + :new_line: 25 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 25 :text: |2 <span id="LC25" class="line"></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 19 + :new_line: 25 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 20 :text: |2 <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">""</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 20 + :new_line: 26 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 26 :text: |2 <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">""</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 20 + :new_line: 26 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 21 :text: |2 <span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 21 + :new_line: 27 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 27 :text: |2 <span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 21 + :new_line: 27 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 28 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 28 :text: | +<span id="LC28" class="line"></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 28 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 22 :text: |2 <span id="LC29" class="line"> <span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span><span class="n">vars</span><span class="p">,</span> <span class="o">*</span><span class="n">cmd</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 22 + :new_line: 29 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 29 :text: |2 <span id="LC29" class="line"> <span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span><span class="n">vars</span><span class="p">,</span> <span class="o">*</span><span class="n">cmd</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 22 + :new_line: 29 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 23 :text: |2 <span id="LC30" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">read</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 23 + :new_line: 30 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 30 :text: |2 <span id="LC30" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">read</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 23 + :new_line: 30 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 24 :text: |2 <span id="LC31" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">read</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 24 + :new_line: 31 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 31 :text: |2 <span id="LC31" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">read</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 24 + :new_line: 31 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 52764f41e0d..e2db33d8345 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -9,7 +9,7 @@ describe DiffHelper do let(:diffs) { commit.diffs } let(:diff) { diffs.first } let(:diff_refs) { [commit.parent, commit] } - let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs) } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) } describe 'diff_view' do it 'returns a valid value when cookie is set' do diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index a0cbef6e6a4..1cb513d5229 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Diff::File, lib: true do let(:project) { create(:project) } let(:commit) { project.commit(sample_commit.id) } let(:diff) { commit.diffs.first } - let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent, commit]) } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) } describe :diff_lines do let(:diff_lines) { diff_file.diff_lines } diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index d19bf4ac84b..fb5d50a5c68 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -6,11 +6,11 @@ describe Gitlab::Diff::Highlight, lib: true do let(:project) { create(:project) } let(:commit) { project.commit(sample_commit.id) } let(:diff) { commit.diffs.first } - let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent, commit]) } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) } describe '#highlight' do context "with a diff file" do - let(:subject) { Gitlab::Diff::Highlight.new(diff_file).highlight } + let(:subject) { Gitlab::Diff::Highlight.new(diff_file, repository: project.repository).highlight } it 'should return Gitlab::Diff::Line elements' do expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) @@ -41,7 +41,7 @@ describe Gitlab::Diff::Highlight, lib: true do end context "with diff lines" do - let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines).highlight } + let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines, repository: project.repository).highlight } it 'should return Gitlab::Diff::Line elements' do expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) diff --git a/spec/lib/gitlab/diff/line_mapper_spec.rb b/spec/lib/gitlab/diff/line_mapper_spec.rb new file mode 100644 index 00000000000..4e50e03bb7e --- /dev/null +++ b/spec/lib/gitlab/diff/line_mapper_spec.rb @@ -0,0 +1,137 @@ +require 'spec_helper' + +describe Gitlab::Diff::LineMapper, lib: true do + include RepoHelpers + + let(:project) { create(:project) } + let(:repository) { project.repository } + let(:commit) { project.commit(sample_commit.id) } + let(:diffs) { commit.diffs } + let(:diff) { diffs.first } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) } + subject { described_class.new(diff_file) } + + describe '#old_to_new' do + context "with a diff file" do + let(:mapping) do + { + 1 => 1, + 2 => 2, + 3 => 3, + 4 => 4, + 5 => 5, + 6 => 6, + 7 => 7, + 8 => 8, + 9 => nil, + # nil => 9, + 10 => 10, + 11 => 11, + 12 => 12, + 13 => nil, + 14 => nil, + # nil => 15, + # nil => 16, + # nil => 17, + # nil => 18, + # nil => 19, + # nil => 20, + 15 => 21, + 16 => 22, + 17 => 23, + 18 => 24, + 19 => 25, + 20 => 26, + 21 => 27, + # nil => 28, + 22 => 29, + 23 => 30, + 24 => 31, + 25 => 32, + 26 => 33, + 27 => 34, + 28 => 35, + 29 => 36, + 30 => 37 + } + end + + it 'returns the new line number for the old line number' do + mapping.each do |old_line, new_line| + expect(subject.old_to_new(old_line)).to eq(new_line) + end + end + end + + context "without a diff file" do + let(:diff_file) { nil } + + it "returns the same line number" do + expect(subject.old_to_new(100)).to eq(100) + end + end + end + + describe '#new_to_old' do + context "with a diff file" do + let(:mapping) do + { + 1 => 1, + 2 => 2, + 3 => 3, + 4 => 4, + 5 => 5, + 6 => 6, + 7 => 7, + 8 => 8, + # nil => 9, + 9 => nil, + 10 => 10, + 11 => 11, + 12 => 12, + # nil => 13, + # nil => 14, + 13 => nil, + 14 => nil, + 15 => nil, + 16 => nil, + 17 => nil, + 18 => nil, + 19 => nil, + 20 => nil, + 21 => 15, + 22 => 16, + 23 => 17, + 24 => 18, + 25 => 19, + 26 => 20, + 27 => 21, + 28 => nil, + 29 => 22, + 30 => 23, + 31 => 24, + 32 => 25, + 33 => 26, + 34 => 27, + 35 => 28, + 36 => 29, + 37 => 30 + } + end + + it 'returns the old line number for the new line number' do + mapping.each do |new_line, old_line| + expect(subject.new_to_old(new_line)).to eq(old_line) + end + end + end + + context "without a diff file" do + let(:diff_file) { nil } + + it "returns the same line number" do + expect(subject.new_to_old(100)).to eq(100) + end + end + end +end diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb index 1c5bbc47120..5f76b70c6f5 100644 --- a/spec/lib/gitlab/diff/parallel_diff_spec.rb +++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb @@ -8,8 +8,7 @@ describe Gitlab::Diff::ParallelDiff, lib: true do let(:commit) { project.commit(sample_commit.id) } let(:diffs) { commit.diffs } let(:diff) { diffs.first } - let(:diff_refs) { [commit.parent, commit] } - let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs) } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) } subject { described_class.new(diff_file) } let(:parallel_diff_result_array) { YAML.load_file("#{Rails.root}/spec/fixtures/parallel_diff_result.yml") } diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb new file mode 100644 index 00000000000..cf28628cb96 --- /dev/null +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -0,0 +1,341 @@ +require 'spec_helper' + +describe Gitlab::Diff::Position, lib: true do + include RepoHelpers + + let(:project) { create(:project) } + + describe "position for an added file" do + let(:commit) { project.commit("2ea1f3dec713d940208fb5ce4a38765ecb5d3f73") } + + subject do + described_class.new( + old_path: "files/images/wm.svg", + new_path: "files/images/wm.svg", + old_line: nil, + new_line: 5, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" 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_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.added?).to be true + expect(diff_line.new_line).to eq(subject.new_line) + expect(diff_line.text).to eq("+ <desc>Created with Sketch.</desc>") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + + describe "position for a changed file" do + let(:commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") } + + describe "position for an added line" do + subject do + described_class.new( + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 14, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.old_path).to eq(subject.old_path) + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.added?).to be true + expect(diff_line.new_line).to eq(subject.new_line) + expect(diff_line.text).to eq("+ vars = {") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 15) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + + describe "position for an unchanged line" do + subject do + described_class.new( + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: 16, + new_line: 22, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.old_path).to eq(subject.old_path) + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.unchanged?).to be true + expect(diff_line.old_line).to eq(subject.old_line) + expect(diff_line.new_line).to eq(subject.new_line) + expect(diff_line.text).to eq(" unless File.directory?(path)") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, subject.old_line) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + + describe "position for a removed line" do + subject do + described_class.new( + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: 14, + new_line: nil, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.old_path).to eq(subject.old_path) + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.removed?).to be true + expect(diff_line.old_line).to eq(subject.old_line) + expect(diff_line.text).to eq("- options = { chdir: path }") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 13, subject.old_line) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + end + + describe "position for a renamed file" do + let(:commit) { project.commit("6907208d755b60ebeacb2e9dfea74c92c3449a1f") } + + describe "position for an added line" do + subject do + described_class.new( + old_path: "files/js/commit.js.coffee", + new_path: "files/js/commit.coffee", + old_line: nil, + new_line: 4, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.old_path).to eq(subject.old_path) + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.added?).to be true + expect(diff_line.new_line).to eq(subject.new_line) + expect(diff_line.text).to eq("+ new CommitFile(@)") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 5) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + + describe "position for an unchanged line" do + subject do + described_class.new( + old_path: "files/js/commit.js.coffee", + new_path: "files/js/commit.coffee", + old_line: 3, + new_line: 3, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.old_path).to eq(subject.old_path) + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.unchanged?).to be true + expect(diff_line.old_line).to eq(subject.old_line) + expect(diff_line.new_line).to eq(subject.new_line) + expect(diff_line.text).to eq(" $('.files .diff-file').each ->") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, subject.old_line) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + + describe "position for a removed line" do + subject do + described_class.new( + old_path: "files/js/commit.js.coffee", + new_path: "files/js/commit.coffee", + old_line: 4, + new_line: nil, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.old_path).to eq(subject.old_path) + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.removed?).to be true + expect(diff_line.old_line).to eq(subject.old_line) + expect(diff_line.text).to eq("- new CommitFile(this)") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 4, subject.old_line) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + end + + describe "position for a deleted file" do + let(:commit) { project.commit("8634272bfad4cf321067c3e94d64d5a253f8321d") } + + subject do + described_class.new( + old_path: "LICENSE", + new_path: "LICENSE", + old_line: 3, + new_line: nil, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" 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.old_path).to eq(subject.old_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.removed?).to be true + expect(diff_line.old_line).to eq(subject.old_line) + expect(diff_line.text).to eq("-Copyright (c) 2014 gitlabhq") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 0, subject.old_line) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end +end diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb new file mode 100644 index 00000000000..08312e60f4a --- /dev/null +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -0,0 +1,1758 @@ +require 'spec_helper' + +describe Gitlab::Diff::PositionTracer, lib: true do + # Douwe's diary New York City, 2016-06-28 + # -------------------------------------------------------------------------- + # + # Dear diary, + # + # Ideally, we would have a test for every single diff scenario that can + # occur and that the PositionTracer should correctly trace a position + # through, across the following variables: + # + # - Old diff file type: created, changed, renamed, deleted, unchanged (5) + # - Old diff line type: added, removed, unchanged (3) + # - New diff file type: created, changed, renamed, deleted, unchanged (5) + # - New diff line type: added, removed, unchanged (3) + # - Old-to-new diff line change: kept, moved, undone (3) + # + # This adds up to 5 * 3 * 5 * 3 * 3 = 675 different potential scenarios, + # and 675 different tests to cover them all. In reality, it would be fewer, + # since one cannot have a removed line in a created file diff, for example, + # but for the sake of this diary entry, let's be pessimistic. + # + # Writing these tests is a manual and time consuming process, as every test + # requires the manual construction or finding of a combination of diffs that + # create the exact diff scenario we are looking for, and can take between + # 1 and 10 minutes, depending on the farfetchedness of the scenario and + # complexity of creating it. + # + # This means that writing tests to cover all of these scenarios would end up + # taking between 11 and 112 hours in total, which I do not believe is the + # best use of my time. + # + # A better course of action would be to think of scenarios that are likely + # to occur, but also potentially tricky to trace correctly, and only cover + # those, with a few more obvious scenarios thrown in to cover our bases. + # + # Unfortunately, I only came to the above realization once I was about + # 1/5th of the way through the process of writing ALL THE SPECS, having + # already wasted about 3 hours trying to be thorough. + # + # I did find 2 bugs while writing those though, so that's good. + # + # In any case, all of this means that the tests below will be extremely + # (excessively, unjustifiably) thorough for scenarios where "the file was + # created in the old diff" and then drop off to comparitively lackluster + # testing of other scenarios. + # + # I did still try to cover most of the obvious and potentially tricky + # scenarios, though. + + include RepoHelpers + + let(:project) { create(:project) } + let(:current_user) { project.owner } + let(:repository) { project.repository } + let(:file_name) { "test-file" } + let(:new_file_name) { "#{file_name}-new" } + let(:second_file_name) { "#{file_name}-2" } + let(:branch_name) { "position-tracer-test" } + + let(:old_diff_refs) { raise NotImplementedError } + let(:new_diff_refs) { raise NotImplementedError } + let(:old_position) { raise NotImplementedError } + + let(:position_tracer) { described_class.new(repository: project.repository, old_diff_refs: old_diff_refs, new_diff_refs: new_diff_refs) } + subject { position_tracer.trace(old_position) } + + def diff_refs(base_commit, head_commit) + Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id) + end + + def position(attrs = {}) + attrs.reverse_merge!( + diff_refs: old_diff_refs + ) + Gitlab::Diff::Position.new(attrs) + end + + def expect_new_position(attrs, new_position = subject) + if attrs.nil? + expect(new_position).to be_nil + else + expect(new_position).not_to be_nil + + expect(new_position.diff_refs).to eq(new_diff_refs) + + attrs.each do |attr, value| + expect(new_position.send(attr)).to eq(value) + end + end + end + + def create_branch(new_name, branch_name) + CreateBranchService.new(project, current_user).execute(new_name, branch_name) + end + + def create_file(branch_name, file_name, content) + Files::CreateService.new( + project, + current_user, + source_branch: branch_name, + target_branch: branch_name, + commit_message: "Create file", + file_path: file_name, + file_content: content + ).execute + project.commit(branch_name) + end + + def update_file(branch_name, file_name, content) + Files::UpdateService.new( + project, + current_user, + source_branch: branch_name, + target_branch: branch_name, + commit_message: "Update file", + file_path: file_name, + file_content: content + ).execute + project.commit(branch_name) + end + + def delete_file(branch_name, file_name) + Files::DeleteService.new( + project, + current_user, + source_branch: branch_name, + target_branch: branch_name, + commit_message: "Delete file", + file_path: file_name + ).execute + project.commit(branch_name) + end + + let(:initial_commit) do + create_branch(branch_name, "master")[:branch].name + project.commit(branch_name) + end + + describe "#trace" do + describe "diff scenarios" do + let(:create_file_commit) do + initial_commit + + create_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + B + C + CONTENT + ) + end + + let(:create_second_file_commit) do + create_file_commit + + create_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + E + CONTENT + ) + end + + let(:update_line_commit) do + create_second_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + CONTENT + ) + end + + let(:update_second_file_line_commit) do + update_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + EE + CONTENT + ) + end + + let(:move_line_commit) do + update_second_file_line_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + BB + A + C + CONTENT + ) + end + + let(:add_second_file_line_commit) do + move_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + EE + F + CONTENT + ) + end + + let(:move_second_file_line_commit) do + add_second_file_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + F + EE + CONTENT + ) + end + + let(:delete_line_commit) do + move_second_file_line_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + BB + A + CONTENT + ) + end + + let(:delete_second_file_line_commit) do + delete_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + F + CONTENT + ) + end + + let(:delete_file_commit) do + delete_second_file_line_commit + + delete_file(branch_name, file_name) + end + + let(:rename_file_commit) do + delete_file_commit + + create_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + BB + A + CONTENT + ) + end + + let(:update_line_again_commit) do + rename_file_commit + + update_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + BB + AA + CONTENT + ) + end + + let(:move_line_again_commit) do + update_line_again_commit + + update_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + AA + BB + CONTENT + ) + end + + let(:delete_line_again_commit) do + move_line_again_commit + + update_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + AA + CONTENT + ) + end + + context "when the file was created in the old diff" do + context "when the file is created in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 + A + # 2 + B + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 + BB + # 2 + A + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: 1 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 + A + # 2 + BB + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + end + + context "when the file is changed in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 - A + # 2 1 BB + # 2 + A + # 3 3 C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 - A + # 2 1 BB + # 2 + A + # 3 3 C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: 1 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 1 BB + # 2 2 A + # 3 - C + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + end + + context "when the file is renamed in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, rename_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 1 BB + # 2 2 A + + it "returns the new position" do + expect_new_position( + old_path: file_name, + new_path: new_file_name, + old_line: old_position.new_line, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 1 BB + # 2 - A + # 2 + AA + + it "returns the new position" do + expect_new_position( + old_path: file_name, + new_path: new_file_name, + old_line: old_position.new_line, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, move_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 + AA + # 1 2 BB + # 2 - A + + it "returns the new position" do + expect_new_position( + old_path: file_name, + new_path: new_file_name, + old_line: 1, + new_line: 2 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 1 BB + # 2 - A + # 2 + AA + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 - BB + # 2 - A + # 1 + AA + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + end + + context "when the file is deleted in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # 1 - BB + # 2 - A + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 - BB + # 2 - A + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 - BB + # 2 - A + # 3 - C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 - A + # 2 - BB + # 3 - C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 - BB + # 2 - A + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + end + + context "when the file is unchanged in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 2 B + # 3 3 C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 2 BB + # 3 3 C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, move_second_file_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 1 BB + # 2 2 A + # 3 3 C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 2 BB + # 3 3 C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_second_file_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 1 BB + # 2 2 A + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + end + end + + context "when the file was changed in the old diff" do + context "when the file is created in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + BB + # 2 + A + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + BB + # 2 + A + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was changed or deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + A + # 2 + B + # 3 + C + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + + context "when the position pointed at a deleted line in the old diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when the position pointed at an unchanged line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 1) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 2) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + BB + # 2 + A + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2, new_line: 2) } + + # old diff: + # 1 1 BB + # 2 2 A + # 3 - C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was changed or deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 3, new_line: 3) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + A + # 2 + B + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + end + + context "when the file is changed in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 1 BB + # 2 2 A + # 3 - C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: 1, + new_line: 1 + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 - A + # 2 1 BB + # 2 + A + # 3 3 C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: 2, + new_line: 1 + ) + end + end + + context "when that line was changed or deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + + context "when the position pointed at a deleted line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: old_position.old_line, + new_line: nil + ) + end + end + end + end + end + end + end + + describe "typical use scenarios" do + let(:second_branch_name) { "#{branch_name}-2" } + + def expect_positions(old_attrs, new_attrs) + old_positions = old_attrs.map do |old_attrs| + position(old_attrs) + end + + new_positions = old_positions.map do |old_position| + position_tracer.trace(old_position) + end + + new_positions.zip(new_attrs).each do |new_position, new_attrs| + expect_new_position(new_attrs, new_position) + end + end + + let(:create_file_commit) do + initial_commit + + create_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + B + C + D + E + F + CONTENT + ) + end + + let(:second_create_file_commit) do + create_file_commit + + create_branch(second_branch_name, branch_name) + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + Z + Z + Z + A + B + C + D + E + F + CONTENT + ) + end + + let(:update_file_commit) do + second_create_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + C + DD + E + F + G + CONTENT + ) + end + + let(:update_file_again_commit) do + update_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + D + E + FF + G + CONTENT + ) + end + + describe "simple push of new commit" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 3 2 C + # 4 - D + # 3 + DD + # 5 4 E + # 6 5 F + # 6 + G + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C + { old_path: file_name, old_line: 4 }, # - D + { new_path: file_name, new_line: 3 }, # + DD + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E + { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F + { new_path: file_name, new_line: 6 }, # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2 }, + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, + { old_path: file_name, old_line: 4, new_line: 4 }, + nil, + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, + { old_path: file_name, old_line: 6 }, + { new_path: file_name, new_line: 7 }, + ] + + expect_positions(old_position_attrs, new_position_attrs) + end + end + + describe "force push to overwrite last commit" do + let(:second_create_file_commit) do + create_file_commit + + create_branch(second_branch_name, branch_name) + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + D + E + FF + G + CONTENT + ) + end + + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, second_create_file_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 3 2 C + # 4 - D + # 3 + DD + # 5 4 E + # 6 5 F + # 6 + G + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C + { old_path: file_name, old_line: 4 }, # - D + { new_path: file_name, new_line: 3 }, # + DD + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E + { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F + { new_path: file_name, new_line: 6 }, # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2 }, + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, + { old_path: file_name, old_line: 4, new_line: 4 }, + nil, + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, + { old_path: file_name, old_line: 6 }, + { new_path: file_name, new_line: 7 }, + ] + + expect_positions(old_position_attrs, new_position_attrs) + end + end + + describe "force push to delete last commit" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 1 A + # 2 - B + # 3 2 C + # 4 - D + # 3 + DD + # 5 4 E + # 6 5 F + # 6 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 }, # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2 }, + nil, + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, + { old_path: file_name, old_line: 4 }, + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, + { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, + nil, + { new_path: file_name, new_line: 6 }, + ] + + expect_positions(old_position_attrs, new_position_attrs) + end + end + + describe "rebase on top of target branch" do + let(:second_update_file_commit) do + update_file_commit + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + Z + Z + Z + A + C + DD + E + F + G + CONTENT + ) + end + + let(:update_file_again_commit) do + second_update_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + D + E + FF + G + CONTENT + ) + end + + let(:overwrite_update_file_again_commit) do + update_file_again_commit + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + Z + Z + Z + A + BB + C + D + E + FF + G + CONTENT + ) + end + + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, overwrite_update_file_again_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 + Z + # 2 + Z + # 3 + Z + # 1 4 A + # 2 - B + # 5 + BB + # 3 6 C + # 4 7 D + # 5 8 E + # 6 - F + # 9 + FF + # 0 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 }, # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 5 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 9 }, # + FF + { new_path: file_name, new_line: 10 }, # + G + ] + + expect_positions(old_position_attrs, new_position_attrs) + end + end + + describe "merge of target branch" do + let(:merge_commit) do + update_file_again_commit + + committer = repository.user_to_committer(current_user) + + options = { + message: "Merge branches", + author: committer, + committer: committer + } + + repository.merge(current_user, second_create_file_commit.sha, branch_name, options) + project.commit(branch_name) + end + + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, merge_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 + Z + # 2 + Z + # 3 + Z + # 1 4 A + # 2 - B + # 5 + BB + # 3 6 C + # 4 7 D + # 5 8 E + # 6 - F + # 9 + FF + # 0 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 }, # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 5 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 9 }, # + FF + { new_path: file_name, new_line: 10 }, # + G + ] + + expect_positions(old_position_attrs, new_position_attrs) + end + end + + describe "changing target branch" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 1 A + # 2 + BB + # 2 3 C + # 3 - DD + # 4 + D + # 4 5 E + # 5 - F + # 6 + FF + # 7 G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 }, # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + nil, + { new_path: file_name, new_line: 2 }, + { old_path: file_name, new_path: file_name, old_line: 2, new_line: 3 }, + { new_path: file_name, new_line: 4 }, + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 5 }, + { old_path: file_name, old_line: 5 }, + { new_path: file_name, new_line: 6 }, + { new_path: file_name, new_line: 7 }, + ] + + expect_positions(old_position_attrs, new_position_attrs) + end + end + end +end diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb index 3cb634ba010..fc9d5204148 100644 --- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -2,17 +2,18 @@ require 'spec_helper' describe Gitlab::GithubImport::BranchFormatter, lib: true do let(:project) { create(:project) } + let(:commit) { create(:commit, project: project) } let(:repo) { double } let(:raw) do { ref: 'feature', repo: repo, - sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + sha: commit.id } end describe '#exists?' do - it 'returns true when branch exists' do + it 'returns true when both branch, and commit exists' do branch = described_class.new(project, double(raw)) expect(branch.exists?).to eq true @@ -23,6 +24,12 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do expect(branch.exists?).to eq false end + + it 'returns false when commit does not exist' do + branch = described_class.new(project, double(raw.merge(sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'))) + + expect(branch.exists?).to eq false + end end describe '#name' do @@ -33,7 +40,7 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do end it 'returns formatted ref when branch does not exist' do - branch = described_class.new(project, double(raw.merge(ref: 'removed-branch'))) + branch = described_class.new(project, double(raw.merge(ref: 'removed-branch', sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'))) expect(branch.name).to eq 'removed-branch-2e5d3239' end @@ -51,18 +58,18 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do it 'returns raw sha' do branch = described_class.new(project, double(raw)) - expect(branch.sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + expect(branch.sha).to eq commit.id end end describe '#valid?' do - it 'returns true when repository exists' do + it 'returns true when raw repo is present' do branch = described_class.new(project, double(raw)) expect(branch.valid?).to eq true end - it 'returns false when repository does not exist' do + it 'returns false when raw repo is blank' do branch = described_class.new(project, double(raw.merge(repo: nil))) expect(branch.valid?).to eq false diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 120f59e6e71..79931ecd134 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -2,11 +2,13 @@ require 'spec_helper' describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:project) { create(:project) } + let(:source_sha) { create(:commit, project: project).id } + let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } let(:repository) { double(id: 1, fork: false) } let(:source_repo) { repository } - let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } + let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: source_sha) } let(:target_repo) { repository } - let(:target_branch) { double(ref: 'master', repo: target_repo, sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7') } + let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) } let(:octocat) { double(id: 123456, login: 'octocat') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -41,10 +43,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', - head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', + source_branch_sha: source_sha, target_project: project, target_branch: 'master', - base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', + target_branch_sha: target_sha, state: 'opened', milestone: nil, author_id: project.creator_id, @@ -68,10 +70,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', - head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', + source_branch_sha: source_sha, target_project: project, target_branch: 'master', - base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', + target_branch_sha: target_sha, state: 'closed', milestone: nil, author_id: project.creator_id, @@ -95,10 +97,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', - head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', + source_branch_sha: source_sha, target_project: project, target_branch: 'master', - base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', + target_branch_sha: target_sha, state: 'merged', milestone: nil, author_id: project.creator_id, diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index a75eaa4d51f..1424de9e60b 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -125,7 +125,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do ci_pipeline = create(:ci_pipeline, project: project, - sha: merge_request.last_commit.id, + sha: merge_request.diff_head_sha, ref: merge_request.source_branch, statuses: [commit_status]) diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb index e848d88182f..3d6bcdfd873 100644 --- a/spec/lib/gitlab/note_data_builder_spec.rb +++ b/spec/lib/gitlab/note_data_builder_spec.rb @@ -27,7 +27,7 @@ describe 'Gitlab::NoteDataBuilder', lib: true do end describe 'When asking for a note on commit diff' do - let(:note) { create(:note_on_commit_diff, project: project) } + let(:note) { create(:diff_note_on_commit, project: project) } it 'returns the note and commit-specific data' do expect(data).to have_key(:commit) @@ -90,7 +90,7 @@ describe 'Gitlab::NoteDataBuilder', lib: true do end let(:note) do - create(:note_on_merge_request_diff, noteable: merge_request, + create(:diff_note_on_merge_request, noteable: merge_request, project: project) end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index bf11472407a..a826b24419a 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -43,9 +43,9 @@ describe Gitlab::UrlBuilder, lib: true do end end - context 'on a CommitDiff' do + context 'on a Commit Diff' do it 'returns a proper URL' do - note = build_stubbed(:note_on_commit_diff) + note = build_stubbed(:diff_note_on_commit) url = described_class.build(note) @@ -75,10 +75,10 @@ describe Gitlab::UrlBuilder, lib: true do end end - context 'on a MergeRequestDiff' do + context 'on a MergeRequest Diff' do it 'returns a proper URL' do merge_request = create(:merge_request, iid: 42) - note = build_stubbed(:note_on_merge_request_diff, noteable: merge_request) + note = build_stubbed(:diff_note_on_merge_request, noteable: merge_request) url = described_class.build(note) diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index aa382f930d7..0a9b10bebea 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -948,7 +948,7 @@ describe Notify do let(:commits) { Commit.decorate(compare.commits, nil) } let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) } let(:send_from_committer_email) { false } - let(:diff_refs) { [project.merge_base_commit(sample_image_commit.id, sample_commit.id), project.commit(sample_commit.id)] } + let(:diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) } subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, send_from_committer_email: send_from_committer_email) } @@ -1049,7 +1049,7 @@ describe Notify do let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) } let(:commits) { Commit.decorate(compare.commits, nil) } let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) } - let(:diff_refs) { [project.merge_base_commit(sample_commit.parent_id, sample_commit.id), project.commit(sample_commit.id)] } + let(:diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) } subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) } diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb new file mode 100644 index 00000000000..af8e890ca95 --- /dev/null +++ b/spec/models/diff_note_spec.rb @@ -0,0 +1,191 @@ +require 'spec_helper' + +describe DiffNote, models: true do + include RepoHelpers + + let(:project) { create(:project) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:commit) { project.commit(sample_commit.id) } + + let(:path) { "files/ruby/popen.rb" } + + let!(:position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + old_line: nil, + new_line: 14, + diff_refs: merge_request.diff_refs + ) + end + + let!(:new_position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + old_line: 16, + new_line: 22, + diff_refs: merge_request.diff_refs + ) + end + + subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } + + describe "#position=" do + context "when provided a string" do + it "sets the position" do + subject.position = new_position.to_json + + expect(subject.position).to eq(new_position) + end + end + + context "when provided a hash" do + it "sets the position" do + subject.position = new_position.to_h + + expect(subject.position).to eq(new_position) + end + end + + context "when provided a position object" do + it "sets the position" do + subject.position = new_position + + expect(subject.position).to eq(new_position) + end + end + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file + + expect(diff_file.old_path).to eq(position.old_path) + expect(diff_file.new_path).to eq(position.new_path) + expect(diff_file.diff_refs).to eq(position.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line + + expect(diff_line.added?).to be true + expect(diff_line.new_line).to eq(position.new_line) + expect(diff_line.text).to eq("+ vars = {") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(position.file_path, position.new_line, 15) + + expect(subject.line_code).to eq(line_code) + end + end + + describe "#for_line?" do + context "when provided the correct diff line" do + it "returns true" do + expect(subject.for_line?(subject.diff_line)).to be true + end + end + + context "when provided a different diff line" do + it "returns false" do + some_line = subject.diff_file.diff_lines.first + + expect(subject.for_line?(some_line)).to be false + end + end + end + + describe "#active?" do + context "when noteable is a commit" do + subject { create(:diff_note_on_commit, project: project, position: position) } + + it "returns true" do + expect(subject.active?).to be true + end + end + + context "when noteable is a merge request" do + context "when the merge request's diff refs match that of the diff note" do + it "returns true" do + expect(subject.active?).to be true + end + end + + 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_refs).and_return(commit.diff_refs) + end + + it "returns false" do + expect(subject.active?).to be false + end + end + end + end + + describe "creation" do + describe "updating of position" do + context "when noteable is a commit" do + let(:diff_note) { create(:diff_note_on_commit, project: project, position: position) } + + it "doesn't use the DiffPositionUpdateService" do + expect(Notes::DiffPositionUpdateService).not_to receive(:new) + + diff_note + end + + it "doesn't update the position" do + diff_note + + expect(diff_note.original_position).to eq(position) + expect(diff_note.position).to eq(position) + end + end + + context "when noteable is a merge request" do + let(:diff_note) { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } + + context "when the note is active" do + it "doesn't use the DiffPositionUpdateService" do + expect(Notes::DiffPositionUpdateService).not_to receive(:new) + + diff_note + end + + it "doesn't update the position" do + diff_note + + expect(diff_note.original_position).to eq(position) + expect(diff_note.position).to eq(position) + end + end + + context "when the note is outdated" do + before do + allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs) + end + + it "uses the DiffPositionUpdateService" do + service = instance_double("Notes::DiffPositionUpdateService") + expect(Notes::DiffPositionUpdateService).to receive(:new).with( + project, + nil, + old_diff_refs: position.diff_refs, + new_diff_refs: commit.diff_refs, + paths: [path] + ).and_return(service) + expect(service).to receive(:execute) + + diff_note + end + end + end + end + end +end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 6ac19756f15..b5d0d79e14e 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -56,7 +56,7 @@ describe Event, models: true do end context 'merge request diff note event' do - let(:target) { create(:note_on_merge_request_diff) } + let(:target) { create(:legacy_diff_note_on_merge_request) } it { is_expected.to be_note } end @@ -132,7 +132,7 @@ describe Event, models: true do context 'merge request diff note event' do let(:project) { create(:project, :public) } let(:merge_request) { create(:merge_request, source_project: project, author: author, assignee: assignee) } - let(:note_on_merge_request) { create(:note_on_merge_request_diff, noteable: merge_request, project: project) } + let(:note_on_merge_request) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project) } let(:target) { note_on_merge_request } it { expect(event.visible_to_user?(non_member)).to eq true } diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb index b2d06853886..d64d89edbd3 100644 --- a/spec/models/legacy_diff_note_spec.rb +++ b/spec/models/legacy_diff_note_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe LegacyDiffNote, models: true do describe "Commit diff line notes" do - let!(:note) { create(:note_on_commit_diff, note: "+1 from me") } + let!(:note) { create(:legacy_diff_note_on_commit, note: "+1 from me") } let!(:commit) { note.noteable } it "should save a valid note" do @@ -17,7 +17,7 @@ describe LegacyDiffNote, models: true do describe '#active?' do it 'is always true when the note has no associated diff' do - note = build(:note_on_merge_request_diff) + note = build(:legacy_diff_note_on_merge_request) expect(note).to receive(:diff).and_return(nil) @@ -25,7 +25,7 @@ describe LegacyDiffNote, models: true do end it 'is never true when the note has no noteable associated' do - note = build(:note_on_merge_request_diff) + note = build(:legacy_diff_note_on_merge_request) expect(note).to receive(:diff).and_return(double) expect(note).to receive(:noteable).and_return(nil) @@ -34,7 +34,7 @@ describe LegacyDiffNote, models: true do end it 'returns the memoized value if defined' do - note = build(:note_on_merge_request_diff) + note = build(:legacy_diff_note_on_merge_request) note.instance_variable_set(:@active, 'foo') expect(note).not_to receive(:find_noteable_diff) @@ -45,7 +45,7 @@ describe LegacyDiffNote, models: true do context 'for a merge request noteable' do it 'is false when noteable has no matching diff' do merge = build_stubbed(:merge_request, :simple) - note = build(:note_on_merge_request_diff, noteable: merge) + note = build(:legacy_diff_note_on_merge_request, noteable: merge) allow(note).to receive(:diff).and_return(double) expect(note).to receive(:find_noteable_diff).and_return(nil) @@ -63,9 +63,9 @@ describe LegacyDiffNote, models: true do code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos) # We're persisting in order to trigger the set_diff callback - note = create(:note_on_merge_request_diff, noteable: merge, - line_code: code, - project: merge.source_project) + note = create(:legacy_diff_note_on_merge_request, noteable: merge, + line_code: code, + project: merge.source_project) # Make sure we don't get a false positive from a guard clause expect(note).to receive(:find_noteable_diff).and_call_original diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index ceb4d64698f..a4b6ff8f8ad 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe MergeRequest, models: true do + include RepoHelpers + subject { create(:merge_request) } describe 'associations' do @@ -62,7 +64,7 @@ describe MergeRequest, models: true do end end - describe '#target_sha' do + describe '#target_branch_sha' do context 'when the target branch does not exist anymore' do let(:project) { create(:project) } @@ -73,32 +75,32 @@ describe MergeRequest, models: true do end it 'returns nil' do - expect(subject.target_sha).to be_nil + expect(subject.target_branch_sha).to be_nil end end end - describe '#source_sha' do + describe '#source_branch_sha' do let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) } context 'with diffs' do subject { create(:merge_request, :with_diffs) } it 'returns the sha of the source branch last commit' do - expect(subject.source_sha).to eq(last_branch_commit.sha) + expect(subject.source_branch_sha).to eq(last_branch_commit.sha) end end context 'without diffs' do subject { create(:merge_request, :without_diffs) } it 'returns the sha of the source branch last commit' do - expect(subject.source_sha).to eq(last_branch_commit.sha) + expect(subject.source_branch_sha).to eq(last_branch_commit.sha) end end context 'when the merge request is being created' do subject { build(:merge_request, source_branch: nil, compare_commits: []) } it 'returns nil' do - expect(subject.source_sha).to be_nil + expect(subject.source_branch_sha).to be_nil end end end @@ -252,12 +254,14 @@ describe MergeRequest, models: true do end it "can be removed if the last commit is the head of the source branch" do - allow(subject.source_project).to receive(:commit).and_return(subject.last_commit) + allow(subject.source_project).to receive(:commit).and_return(subject.diff_head_commit) expect(subject.can_remove_source_branch?(user)).to be_truthy end it "cannot be removed if the last commit is not also the head of the source branch" do + subject.source_branch = "lfs" + expect(subject.can_remove_source_branch?(user)).to be_falsey end end @@ -363,7 +367,7 @@ describe MergeRequest, models: true do and_return(2) subject.diverged_commits_count - allow(subject).to receive(:source_sha).and_return('123abc') + allow(subject).to receive(:source_branch_sha).and_return('123abc') subject.diverged_commits_count end @@ -373,7 +377,7 @@ describe MergeRequest, models: true do and_return(2) subject.diverged_commits_count - allow(subject).to receive(:target_sha).and_return('123abc') + allow(subject).to receive(:target_branch_sha).and_return('123abc') subject.diverged_commits_count end end @@ -392,11 +396,10 @@ describe MergeRequest, models: true do describe '#pipeline' do describe 'when the source project exists' do - it 'returns the latest commit' do - commit = double(:commit, id: '123abc') + it 'returns the latest pipeline' do pipeline = double(:ci_pipeline, ref: 'master') - allow(subject).to receive(:last_commit).and_return(commit) + allow(subject).to receive(:diff_head_sha).and_return('123abc') expect(subject.source_project).to receive(:pipeline). with('123abc', 'master'). @@ -464,7 +467,7 @@ describe MergeRequest, models: true do context 'when it is not broken and has no conflicts' do it 'is marked as mergeable' do allow(subject).to receive(:broken?) { false } - allow(project.repository).to receive(:can_be_merged?) { true } + allow(project.repository).to receive(:can_be_merged?).and_return(true) expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged') end @@ -481,7 +484,7 @@ describe MergeRequest, models: true do context 'when it has conflicts' do before do allow(subject).to receive(:broken?) { false } - allow(project.repository).to receive(:can_be_merged?) { false } + allow(project.repository).to receive(:can_be_merged?).and_return(false) end it 'becomes unmergeable' do @@ -608,4 +611,42 @@ describe MergeRequest, models: true do end end end + + describe "#reload_diff" do + let(:note) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject) } + + let(:commit) { subject.project.commit(sample_commit.id) } + + it "reloads the diff content" do + expect(subject.merge_request_diff).to receive(:reload_content) + + subject.reload_diff + end + + it "updates diff note positions" do + old_diff_refs = subject.diff_refs + + merge_request_diff = subject.merge_request_diff + + # Update merge_request_diff so that #diff_refs will return commit.diff_refs + allow(merge_request_diff).to receive(:reload_content) do + merge_request_diff.base_commit_sha = commit.parent_id + merge_request_diff.start_commit_sha = commit.parent_id + merge_request_diff.head_commit_sha = commit.sha + end + + expect(Notes::DiffPositionUpdateService).to receive(:new).with( + subject.project, + nil, + old_diff_refs: old_diff_refs, + new_diff_refs: commit.diff_refs, + paths: note.position.paths + ).and_call_original + expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note) + + expect_any_instance_of(DiffNote).to receive(:save).once + + subject.reload_diff + end + end end diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb index df336a6effe..d58673413c8 100644 --- a/spec/models/notification_setting_spec.rb +++ b/spec/models/notification_setting_spec.rb @@ -38,4 +38,21 @@ RSpec.describe NotificationSetting, type: :model do end end end + + describe '#for_projects' do + let(:user) { create(:user) } + + before do + 1.upto(4) do |i| + setting = create(:notification_setting, user: user) + + setting.project.update_attributes(pending_delete: true) if i.even? + end + end + + it 'excludes projects pending delete' do + expect(user.notification_settings.for_projects).to all(have_attributes(project: an_instance_of(Project))) + expect(user.notification_settings.for_projects.map(&:project)).to all(have_attributes(pending_delete: false)) + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2e89d6de3a2..1b434a726dc 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -312,7 +312,7 @@ describe Project, models: true do it 'should update merge request commits with new one if pushed to source branch' do project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.source_branch}", key.user) merge_request.reload - expect(merge_request.last_commit.id).to eq(commit_id) + expect(merge_request.diff_head_sha).to eq(commit_id) end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 7975fc64e59..24e49c8def3 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -12,8 +12,8 @@ describe Repository, models: true do end let(:merge_commit) do source_sha = repository.find_branch('feature').target - merge_commit_id = repository.merge(user, source_sha, 'master', commit_options) - repository.commit(merge_commit_id) + merge_commit_sha = repository.merge(user, source_sha, 'master', commit_options) + repository.commit(merge_commit_sha) end describe :branch_names_contains do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 5d81844fb84..4a1b5600bdf 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -439,14 +439,14 @@ describe API::API, api: true do end it "returns 409 if the SHA parameter doesn't match" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.source_sha.succ + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse expect(response).to have_http_status(409) expect(json_response['message']).to start_with('SHA does not match HEAD of source branch') end it "succeeds if the SHA parameter matches" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.source_sha + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha expect(response).to have_http_status(200) end diff --git a/spec/services/notes/diff_position_update_service_spec.rb b/spec/services/notes/diff_position_update_service_spec.rb new file mode 100644 index 00000000000..110efb54fa0 --- /dev/null +++ b/spec/services/notes/diff_position_update_service_spec.rb @@ -0,0 +1,175 @@ +require 'spec_helper' + +describe Notes::DiffPositionUpdateService, services: true do + let(:project) { create(:project) } + let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") } + let(:modify_commit) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e") } + let(:edit_commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") } + + let(:path) { "files/ruby/popen.rb" } + + let(:old_diff_refs) do + Gitlab::Diff::DiffRefs.new( + base_sha: create_commit.parent_id, + head_sha: modify_commit.sha + ) + end + + let(:new_diff_refs) do + Gitlab::Diff::DiffRefs.new( + base_sha: create_commit.parent_id, + head_sha: edit_commit.sha + ) + end + + subject do + described_class.new( + project, + nil, + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs, + paths: [path] + ) + end + + # old diff: + # 1 + require 'fileutils' + # 2 + require 'open3' + # 3 + + # 4 + module Popen + # 5 + extend self + # 6 + + # 7 + def popen(cmd, path=nil) + # 8 + unless cmd.is_a?(Array) + # 9 + raise "System commands must be given as an array of strings" + # 10 + end + # 11 + + # 12 + path ||= Dir.pwd + # 13 + vars = { "PWD" => path } + # 14 + options = { chdir: path } + # 15 + + # 16 + unless File.directory?(path) + # 17 + FileUtils.mkdir_p(path) + # 18 + end + # 19 + + # 20 + @cmd_output = "" + # 21 + @cmd_status = 0 + # 22 + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + # 23 + @cmd_output << stdout.read + # 24 + @cmd_output << stderr.read + # 25 + @cmd_status = wait_thr.value.exitstatus + # 26 + end + # 27 + + # 28 + return @cmd_output, @cmd_status + # 29 + end + # 30 + end + # + # new diff: + # 1 + require 'fileutils' + # 2 + require 'open3' + # 3 + + # 4 + module Popen + # 5 + extend self + # 6 + + # 7 + def popen(cmd, path=nil) + # 8 + unless cmd.is_a?(Array) + # 9 + raise RuntimeError, "System commands must be given as an array of strings" + # 10 + end + # 11 + + # 12 + path ||= Dir.pwd + # 13 + + # 14 + vars = { + # 15 + "PWD" => path + # 16 + } + # 17 + + # 18 + options = { + # 19 + chdir: path + # 20 + } + # 21 + + # 22 + unless File.directory?(path) + # 23 + FileUtils.mkdir_p(path) + # 24 + end + # 25 + + # 26 + @cmd_output = "" + # 27 + @cmd_status = 0 + # 28 + + # 29 + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + # 30 + @cmd_output << stdout.read + # 31 + @cmd_output << stderr.read + # 32 + @cmd_status = wait_thr.value.exitstatus + # 33 + end + # 34 + + # 35 + return @cmd_output, @cmd_status + # 36 + end + # 37 + end + # + # old->new diff: + # .. .. @@ -6,12 +6,18 @@ module Popen + # 6 6 + # 7 7 def popen(cmd, path=nil) + # 8 8 unless cmd.is_a?(Array) + # 9 - raise "System commands must be given as an array of strings" + # 9 + raise RuntimeError, "System commands must be given as an array of strings" + # 10 10 end + # 11 11 + # 12 12 path ||= Dir.pwd + # 13 - vars = { "PWD" => path } + # 14 - options = { chdir: path } + # 13 + + # 14 + vars = { + # 15 + "PWD" => path + # 16 + } + # 17 + + # 18 + options = { + # 19 + chdir: path + # 20 + } + # 15 21 + # 16 22 unless File.directory?(path) + # 17 23 FileUtils.mkdir_p(path) + # 18 24 end + # 19 25 + # 20 26 @cmd_output = "" + # 21 27 @cmd_status = 0 + # 28 + + # 22 29 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + # 23 30 @cmd_output << stdout.read + # 24 31 @cmd_output << stderr.read + # .. .. + + describe "#execute" do + let(:note) { create(:diff_note_on_merge_request, project: project, position: old_position) } + + let(:old_position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + old_line: nil, + new_line: line, + diff_refs: old_diff_refs + ) + end + + context "when the diff line is the same" do + let(:line) { 16 } + + it "updates the position" do + subject.execute(note) + + expect(note.original_position).to eq(old_position) + expect(note.position).not_to eq(old_position) + expect(note.position.new_line).to eq(22) + end + end + + context "when the diff line has changed" do + let(:line) { 9 } + + it "doesn't update the position" do + subject.execute(note) + + expect(note.original_position).to eq(old_position) + expect(note.position).to eq(old_position) + end + end + end +end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 85dd30bf48c..43693441450 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -213,7 +213,7 @@ describe SystemNoteService, services: true do create(:merge_request, source_project: project, target_project: project) end - subject { described_class.merge_when_build_succeeds(noteable, project, author, noteable.last_commit) } + subject { described_class.merge_when_build_succeeds(noteable, project, author, noteable.diff_head_commit) } it_behaves_like 'a system note' |