Merge branch 'single-file-diffs' into 'master'

Single file diffs

Closes #14103. Related (not part of this MR): #19553.

This adds a `diff_for_path` action to each place we render diffs (commit, compare, new MR, existing MR) which renders the diff for a single path. The action is always available with the same params as the 'parent' action, to make it simpler to generate the URIs.

If a diff is bigger than 10 KB, it will be collapsed by default and have a data attribute added. You can then click the message or the filename to expand that diff. For expanded files, you can collapse and expand them, but they won't make any AJAX requests.

![Expand_and_collapse_diffs](/uploads/a4072029085082b85c47006f67ac531c/Expand_and_collapse_diffs.gif)

See merge request !4990
This commit is contained in:
Douwe Maan 2016-07-11 17:51:57 +00:00
commit ef9ba9050e
33 changed files with 1123 additions and 509 deletions

View file

@ -36,6 +36,8 @@ v 8.10.0 (unreleased)
- Only show New Snippet button to users that can create snippets. - Only show New Snippet button to users that can create snippets.
- PipelinesFinder uses git cache data - PipelinesFinder uses git cache data
- Throttle the update of `project.pushes_since_gc` to 1 minute. - Throttle the update of `project.pushes_since_gc` to 1 minute.
- Allow expanding and collapsing files in diff view (!4990)
- Collapse large diffs by default (!4990)
- Check for conflicts with existing Project's wiki path when creating a new project. - Check for conflicts with existing Project's wiki path when creating a new project.
- Show last push widget in upstream after push to fork - Show last push widget in upstream after push to fork
- Don't instantiate a git tree on Projects show default view - Don't instantiate a git tree on Projects show default view

View file

@ -1,6 +1,8 @@
class @Diff class @Diff
UNFOLD_COUNT = 20 UNFOLD_COUNT = 20
constructor: -> constructor: ->
$('.files .diff-file').singleFileDiff()
$(document).off('click', '.js-unfold') $(document).off('click', '.js-unfold')
$(document).on('click', '.js-unfold', (event) => $(document).on('click', '.js-unfold', (event) =>
target = $(event.target) target = $(event.target)

View file

@ -160,6 +160,7 @@ class @MergeRequestTabs
$('#diffs').html data.html $('#diffs').html data.html
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')) gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'))
$('#diffs .js-syntax-highlight').syntaxHighlight() $('#diffs .js-syntax-highlight').syntaxHighlight()
$('#diffs .diff-file').singleFileDiff()
@expandViewContainer() if @diffViewType() is 'parallel' @expandViewContainer() if @diffViewType() is 'parallel'
@diffsLoaded = true @diffsLoaded = true
@scrollToElement("#diffs") @scrollToElement("#diffs")

View file

@ -0,0 +1,54 @@
class @SingleFileDiff
WRAPPER = '<div class="diff-content diff-wrap-lines"></div>'
LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'
ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>'
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>'
constructor: (@file) ->
@content = $('.diff-content', @file)
@diffForPath = @content.find('[data-diff-for-path]').data 'diff-for-path'
@isOpen = !@diffForPath
if @diffForPath
@collapsedContent = @content
@loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide()
@content = null
@collapsedContent.after(@loadingContent)
else
@collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide()
@content.after(@collapsedContent)
@collapsedContent.on 'click', @toggleDiff
$('.file-title > a', @file).on 'click', @toggleDiff
toggleDiff: (e) =>
@isOpen = !@isOpen
if not @isOpen and not @hasError
@content.hide()
@collapsedContent.show()
else if @content
@collapsedContent.hide()
@content.show()
else
@getContentHTML()
getContentHTML: ->
@collapsedContent.hide()
@loadingContent.show()
$.get @diffForPath, (data) =>
@loadingContent.hide()
if data.html
@content = $(data.html)
@content.syntaxHighlight()
else
@hasError = true
@content = $(ERROR_HTML)
@collapsedContent.after(@content)
return
$.fn.singleFileDiff = ->
return @each ->
if not $.data this, 'singleFileDiff'
$.data this, 'singleFileDiff', new SingleFileDiff this

View file

@ -16,6 +16,9 @@
font-weight: normal; font-weight: normal;
font-size: 16px; font-size: 16px;
line-height: 36px; line-height: 36px;
&.diff-collapsed {
cursor: pointer;
}
} }
.row-content-block { .row-content-block {

View file

@ -0,0 +1,25 @@
module DiffForPath
extend ActiveSupport::Concern
def render_diff_for_path(diffs, diff_refs, project)
diff_file = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository).find do |diff|
diff.old_path == params[:old_path] && diff.new_path == params[:new_path]
end
return render_404 unless diff_file
diff_commit = commit_for_diff(diff_file)
blob = diff_file.blob(diff_commit)
@expand_all_diffs = true
locals = {
diff_file: diff_file,
diff_commit: diff_commit,
diff_refs: diff_refs,
blob: blob,
project: project
}
render json: { html: view_to_html_string('projects/diffs/_content', locals) }
end
end

View file

@ -3,6 +3,7 @@
# Not to be confused with CommitsController, plural. # Not to be confused with CommitsController, plural.
class Projects::CommitController < Projects::ApplicationController class Projects::CommitController < Projects::ApplicationController
include CreatesCommit include CreatesCommit
include DiffForPath
include DiffHelper include DiffHelper
# Authorize # Authorize
@ -11,29 +12,14 @@ class Projects::CommitController < Projects::ApplicationController
before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds] before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds]
before_action :authorize_read_commit_status!, only: [:builds] before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit before_action :commit
before_action :define_show_vars, only: [:show, :builds] before_action :define_commit_vars, only: [:show, :diff_for_path, :builds]
before_action :define_status_vars, only: [:show, :builds]
before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
def show def show
apply_diff_view_cookie! apply_diff_view_cookie!
@grouped_diff_notes = commit.notes.grouped_diff_notes
@notes = commit.notes.non_diff_notes.fresh
Banzai::NoteRenderer.render(
@grouped_diff_notes.values.flatten + @notes,
@project,
current_user,
)
@note = @project.build_commit_note(commit)
@noteable = @commit
@comments_target = {
noteable_type: 'Commit',
commit_id: @commit.id
}
respond_to do |format| respond_to do |format|
format.html format.html
format.diff { render text: @commit.to_diff } format.diff { render text: @commit.to_diff }
@ -41,6 +27,10 @@ class Projects::CommitController < Projects::ApplicationController
end end
end end
def diff_for_path
render_diff_for_path(@diffs, @commit.diff_refs, @project)
end
def builds def builds
end end
@ -114,7 +104,7 @@ class Projects::CommitController < Projects::ApplicationController
@ci_builds ||= Ci::Build.where(pipeline: pipelines) @ci_builds ||= Ci::Build.where(pipeline: pipelines)
end end
def define_show_vars def define_commit_vars
return git_not_found! unless commit return git_not_found! unless commit
opts = diff_options opts = diff_options
@ -122,7 +112,28 @@ class Projects::CommitController < Projects::ApplicationController
@diffs = commit.diffs(opts) @diffs = commit.diffs(opts)
@notes_count = commit.notes.count @notes_count = commit.notes.count
end
def define_note_vars
@grouped_diff_notes = commit.notes.grouped_diff_notes
@notes = commit.notes.non_diff_notes.fresh
Banzai::NoteRenderer.render(
@grouped_diff_notes.values.flatten + @notes,
@project,
current_user,
)
@note = @project.build_commit_note(commit)
@noteable = @commit
@comments_target = {
noteable_type: 'Commit',
commit_id: @commit.id
}
end
def define_status_vars
@statuses = CommitStatus.where(pipeline: pipelines) @statuses = CommitStatus.where(pipeline: pipelines)
@builds = Ci::Build.where(pipeline: pipelines) @builds = Ci::Build.where(pipeline: pipelines)
end end

View file

@ -1,29 +1,51 @@
require 'addressable/uri' require 'addressable/uri'
class Projects::CompareController < Projects::ApplicationController class Projects::CompareController < Projects::ApplicationController
include DiffForPath
include DiffHelper include DiffHelper
# Authorize # Authorize
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :assign_ref_vars, only: [:index, :show] before_action :define_ref_vars, only: [:index, :show, :diff_for_path]
before_action :define_diff_vars, only: [:show, :diff_for_path]
before_action :merge_request, only: [:index, :show] before_action :merge_request, only: [:index, :show]
def index def index
end end
def show def show
compare = CompareService.new. end
execute(@project, @head_ref, @project, @start_ref, diff_options)
if compare def diff_for_path
@commits = Commit.decorate(compare.commits, @project) return render_404 unless @compare
render_diff_for_path(@diffs, @diff_refs, @project)
end
def create
redirect_to namespace_project_compare_path(@project.namespace, @project,
params[:from], params[:to])
end
private
def define_ref_vars
@start_ref = Addressable::URI.unescape(params[:from])
@ref = @head_ref = Addressable::URI.unescape(params[:to])
end
def define_diff_vars
@compare = CompareService.new.execute(@project, @head_ref, @project, @start_ref)
if @compare
@commits = Commit.decorate(@compare.commits, @project)
@start_commit = @project.commit(@start_ref) @start_commit = @project.commit(@start_ref)
@commit = @project.commit(@head_ref) @commit = @project.commit(@head_ref)
@base_commit = @project.merge_base_commit(@start_ref, @head_ref) @base_commit = @project.merge_base_commit(@start_ref, @head_ref)
@diffs = compare.diffs(diff_options) @diffs = @compare.diffs(diff_options)
@diff_refs = Gitlab::Diff::DiffRefs.new( @diff_refs = Gitlab::Diff::DiffRefs.new(
base_sha: @base_commit.try(:sha), base_sha: @base_commit.try(:sha),
start_sha: @start_commit.try(:sha), start_sha: @start_commit.try(:sha),
@ -35,18 +57,6 @@ class Projects::CompareController < Projects::ApplicationController
end end
end end
def create
redirect_to namespace_project_compare_path(@project.namespace, @project,
params[:from], params[:to])
end
private
def assign_ref_vars
@start_ref = Addressable::URI.unescape(params[:from])
@ref = @head_ref = Addressable::URI.unescape(params[:to])
end
def merge_request def merge_request
@merge_request ||= @project.merge_requests.opened. @merge_request ||= @project.merge_requests.opened.
find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref)

View file

@ -1,5 +1,6 @@
class Projects::MergeRequestsController < Projects::ApplicationController class Projects::MergeRequestsController < Projects::ApplicationController
include ToggleSubscriptionAction include ToggleSubscriptionAction
include DiffForPath
include DiffHelper include DiffHelper
include IssuableActions include IssuableActions
include ToggleAwardEmoji include ToggleAwardEmoji
@ -12,6 +13,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
before_action :define_show_vars, only: [:show, :diffs, :commits, :builds] before_action :define_show_vars, only: [:show, :diffs, :commits, :builds]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
before_action :define_commit_vars, only: [:diffs]
before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds]
# Allow read any merge_request # Allow read any merge_request
@ -78,32 +81,31 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request_diff = @merge_request.merge_request_diff @merge_request_diff = @merge_request.merge_request_diff
@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(
@grouped_diff_notes.values.flatten,
@project,
current_user,
@path,
@project_wiki,
@ref
)
respond_to do |format| respond_to do |format|
format.html format.html
format.json { render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } } format.json { render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } }
end end
end end
# With an ID param, loads the MR at that ID. Otherwise, accepts the same params as #new
# and uses that (unsaved) MR.
#
def diff_for_path
if params[:id]
merge_request
define_diff_comment_vars
else
build_merge_request
@diff_notes_disabled = true
@grouped_diff_notes = {}
end
define_commit_vars
diffs = @merge_request.diffs(diff_options)
render_diff_for_path(diffs, @merge_request.diff_refs, @merge_request.project)
end
def commits def commits
respond_to do |format| respond_to do |format|
format.html { render 'show' } format.html { render 'show' }
@ -127,8 +129,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def new def new
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) build_merge_request
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
@noteable = @merge_request @noteable = @merge_request
@target_branches = if @merge_request.target_project @target_branches = if @merge_request.target_project
@ -384,6 +385,30 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@pipelines = [@pipeline].compact @pipelines = [@pipeline].compact
end end
def define_commit_vars
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit
end
def define_diff_comment_vars
@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(
@grouped_diff_notes.values.flatten,
@project,
current_user,
@path,
@project_wiki,
@ref
)
end
def invalid_mr def invalid_mr
# Render special view for MR with removed source or target branch # Render special view for MR with removed source or target branch
render 'invalid' render 'invalid'
@ -412,4 +437,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
params[:merge_when_build_succeeds].present? && params[:merge_when_build_succeeds].present? &&
@merge_request.pipeline && @merge_request.pipeline.active? @merge_request.pipeline && @merge_request.pipeline.active?
end end
def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
end
end end

View file

@ -8,6 +8,10 @@ module DiffHelper
[marked_old_line, marked_new_line] [marked_old_line, marked_new_line]
end end
def expand_all_diffs?
@expand_all_diffs || params[:expand_all_diffs].present?
end
def diff_view def diff_view
diff_views = %w(inline parallel) diff_views = %w(inline parallel)
@ -18,16 +22,14 @@ module DiffHelper
end end
end end
def diff_hard_limit_enabled? def diff_options
params[:force_show_diff].present? default_options = Commit.max_diff_options
if action_name == 'diff_for_path'
default_options[:paths] = params.values_at(:old_path, :new_path)
end end
def diff_options default_options.merge(ignore_whitespace_change: hide_whitespace?)
options = { ignore_whitespace_change: hide_whitespace? }
if diff_hard_limit_enabled?
options.merge!(Commit.max_diff_options)
end
options
end end
def safe_diff_files(diffs, diff_refs: nil, repository: nil) def safe_diff_files(diffs, diff_refs: nil, repository: nil)

View file

@ -19,7 +19,7 @@ class MergeRequest < ActiveRecord::Base
after_create :create_merge_request_diff, unless: :importing? after_create :create_merge_request_diff, unless: :importing?
after_update :update_merge_request_diff after_update :update_merge_request_diff
delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil delegate :commits, :real_size, to: :merge_request_diff, prefix: nil
# When this attribute is true some MR validation is ignored # When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests # It allows us to close or modify broken merge requests
@ -164,6 +164,10 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
end end
def diffs(*args)
merge_request_diff ? merge_request_diff.diffs(*args) : compare.diffs(*args)
end
def diff_size def diff_size
merge_request_diff.size merge_request_diff.size
end end

View file

@ -46,7 +46,8 @@ class MergeRequestDiff < ActiveRecord::Base
compare.diffs(options) compare.diffs(options)
end end
else else
@diffs ||= load_diffs(st_diffs, options) @diffs ||= {}
@diffs[options] ||= load_diffs(st_diffs, options)
end end
end end
@ -144,6 +145,12 @@ class MergeRequestDiff < ActiveRecord::Base
def load_diffs(raw, options) def load_diffs(raw, options)
if raw.respond_to?(:each) if raw.respond_to?(:each)
if paths = options[:paths]
raw = raw.select do |diff|
paths.include?(diff[:old_path]) || paths.include?(diff[:new_path])
end
end
Gitlab::Git::DiffCollection.new(raw, options) Gitlab::Git::DiffCollection.new(raw, options)
else else
Gitlab::Git::DiffCollection.new([]) Gitlab::Git::DiffCollection.new([])

View file

@ -3,7 +3,7 @@ require 'securerandom'
# Compare 2 branches for one repo or between repositories # Compare 2 branches for one repo or between repositories
# and return Gitlab::Git::Compare object that responds to commits and diffs # and return Gitlab::Git::Compare object that responds to commits and diffs
class CompareService class CompareService
def execute(source_project, source_branch, target_project, target_branch, diff_options = {}) def execute(source_project, source_branch, target_project, target_branch)
source_commit = source_project.commit(source_branch) source_commit = source_project.commit(source_branch)
return unless source_commit return unless source_commit

View file

@ -0,0 +1,29 @@
.diff-content.diff-wrap-lines
- # Skip all non non-supported blobs
- 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.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_file.collapsed_by_default? && !expand_all_diffs?
- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path))
.nothing-here-block.diff-collapsed{data: { diff_for_path: url } }
This diff is collapsed. Click to expand it.
- elsif diff_view == 'parallel'
= render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob
- else
= render "projects/diffs/text_file", diff_file: diff_file
- 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_blob = diff_file.old_blob(diff_commit)
= render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob
- else
.nothing-here-block No preview for this file type

View file

@ -6,6 +6,8 @@
.content-block.oneline-block.files-changed .content-block.oneline-block.files-changed
.inline-parallel-buttons .inline-parallel-buttons
- unless expand_all_diffs?
= link_to 'Expand all', url_for(params.merge(expand_all_diffs: 1, format: 'html')), class: 'btn btn-default'
- if show_whitespace_toggle - if show_whitespace_toggle
- if current_controller?(:commit) - if current_controller?(:commit)
= commit_diff_whitespace_link(@project, @commit, class: 'hidden-xs') = commit_diff_whitespace_link(@project, @commit, class: 'hidden-xs')

View file

@ -16,28 +16,4 @@
= view_file_btn(diff_commit.id, diff_file, project) = view_file_btn(diff_commit.id, diff_file, project)
.diff-content.diff-wrap-lines = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, diff_refs: diff_refs, blob: blob, project: project
- # Skip all non non-supported blobs
- 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.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_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

View file

@ -1,9 +1,4 @@
- too_big = diff_file.diff_lines.count > Commit::DIFF_SAFE_LINES %table.text-file.code.js-syntax-highlight
- if too_big
.suppressed-container
%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 - last_line = 0
- diff_file.highlighted_diff_lines.each do |line| - diff_file.highlighted_diff_lines.each do |line|
- last_line = line.new_pos - last_line = line.new_pos

View file

@ -2,9 +2,6 @@
%h4 %h4
Too many changes to show. Too many changes to show.
.pull-right .pull-right
- unless diff_hard_limit_enabled?
= link_to "Reload with full diff", url_for(params.merge(force_show_diff: true, format: nil)), class: "btn btn-sm"
- if current_controller?(:commit) or current_controller?(:merge_requests) - if current_controller?(:commit) or current_controller?(:merge_requests)
- if current_controller?(:commit) - if current_controller?(:commit)
= link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-sm" = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-sm"

View file

@ -615,10 +615,18 @@ Rails.application.routes.draw do
post :retry_builds post :retry_builds
post :revert post :revert
post :cherry_pick post :cherry_pick
get :diff_for_path
end end
end end
resources :compare, only: [:index, :create] resources :compare, only: [:index, :create] do
collection do
get :diff_for_path
end
end
get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
resources :network, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } resources :network, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ }
resources :graphs, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } do resources :graphs, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } do
@ -629,9 +637,6 @@ Rails.application.routes.draw do
end end
end end
get '/compare/:from...:to' => 'compare#show', :as => 'compare',
:constraints => { from: /.+/, to: /.+/ }
resources :snippets, constraints: { id: /\d+/ } do resources :snippets, constraints: { id: /\d+/ } do
member do member do
get 'raw' get 'raw'
@ -706,12 +711,14 @@ Rails.application.routes.draw do
post :toggle_subscription post :toggle_subscription
post :toggle_award_emoji post :toggle_award_emoji
post :remove_wip post :remove_wip
get :diff_for_path
end end
collection do collection do
get :branch_from get :branch_from
get :branch_to get :branch_to
get :update_branches get :update_branches
get :diff_for_path
end end
end end

View file

@ -83,11 +83,6 @@ Feature: Project Commits
#Given I visit my project's commits stats page #Given I visit my project's commits stats page
#Then I see commits stats #Then I see commits stats
Scenario: I browse big commit
Given I visit big commit page
Then I see big commit warning
And I see "Reload with full diff" link
Scenario: I browse a commit with an image Scenario: I browse a commit with an image
Given I visit a commit with an image that changed Given I visit a commit with an image that changed
Then The diff links to both the previous and current image Then The diff links to both the previous and current image

View file

@ -125,25 +125,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(page).to have_content 'Authors' expect(page).to have_content 'Authors'
end end
step 'I visit big commit page' do
# Create a temporary scope to ensure that the stub_const is removed after user
RSpec::Mocks.with_temporary_scope do
stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_lines: 1, max_files: 1 })
visit namespace_project_commit_path(@project.namespace, @project, sample_big_commit.id)
end
end
step 'I see big commit warning' do
expect(page).to have_content sample_big_commit.message
expect(page).to have_content "Too many changes"
end
step 'I see "Reload with full diff" link' do
link = find_link('Reload with full diff')
expect(link[:href]).to end_with('?force_show_diff=true')
expect(link[:href]).not_to include('.html')
end
step 'I visit a commit with an image that changed' do step 'I visit a commit with an image that changed' do
visit namespace_project_commit_path(@project.namespace, @project, sample_image_commit.id) visit namespace_project_commit_path(@project.namespace, @project, sample_image_commit.id)
end end

View file

@ -68,6 +68,10 @@ module Gitlab
@lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a @lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a
end end
def collapsed_by_default?
diff.diff.bytesize > 10240 # 10 KB
end
def highlighted_diff_lines def highlighted_diff_lines
@highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
end end

View file

@ -1,246 +0,0 @@
require 'spec_helper'
describe Projects::CommitController do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:commit) { project.commit("master") }
let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
let(:master_pickable_commit) { project.commit(master_pickable_sha) }
before do
sign_in(user)
project.team << [user, :master]
end
describe "#show" do
shared_examples "export as" do |format|
it "should generally work" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: commit.id,
format: format)
expect(response).to be_success
end
it "should generate it" do
expect_any_instance_of(Commit).to receive(:"to_#{format}")
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: commit.id, format: format)
end
it "should render it" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: commit.id, format: format)
expect(response.body).to eq(commit.send(:"to_#{format}"))
end
it "should not escape Html" do
allow_any_instance_of(Commit).to receive(:"to_#{format}").
and_return('HTML entities &<>" ')
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: commit.id, format: format)
expect(response.body).not_to include('&amp;')
expect(response.body).not_to include('&gt;')
expect(response.body).not_to include('&lt;')
expect(response.body).not_to include('&quot;')
end
end
describe "as diff" do
include_examples "export as", :diff
let(:format) { :diff }
it "should really only be a git diff" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: commit.id,
format: format)
expect(response.body).to start_with("diff --git")
end
it "should really only be a git diff without whitespace changes" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: '66eceea0db202bb39c4e445e8ca28689645366c5',
# id: commit.id,
format: format,
w: 1)
expect(response.body).to start_with("diff --git")
# without whitespace option, there are more than 2 diff_splits
diff_splits = assigns(:diffs).first.diff.split("\n")
expect(diff_splits.length).to be <= 2
end
end
describe "as patch" do
include_examples "export as", :patch
let(:format) { :patch }
it "should really be a git email patch" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: commit.id,
format: format)
expect(response.body).to start_with("From #{commit.id}")
end
it "should contain a git diff" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: commit.id,
format: format)
expect(response.body).to match(/^diff --git/)
end
end
context 'commit that removes a submodule' do
render_views
let(:fork_project) { create(:forked_project_with_submodules) }
let(:commit) { fork_project.commit('remove-submodule') }
before do
fork_project.team << [user, :master]
end
it 'renders it' do
get(:show,
namespace_id: fork_project.namespace.to_param,
project_id: fork_project.to_param,
id: commit.id)
expect(response).to be_success
end
end
end
describe "#branches" do
it "contains branch and tags information" do
get(:branches,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: commit.id)
expect(assigns(:branches)).to include("master", "feature_conflict")
expect(assigns(:tags)).to include("v1.1.0")
end
end
describe '#revert' do
context 'when target branch is not provided' do
it 'should render the 404 page' do
post(:revert,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: commit.id)
expect(response).not_to be_success
expect(response).to have_http_status(404)
end
end
context 'when the revert was successful' do
it 'should redirect to the commits page' do
post(:revert,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
target_branch: 'master',
id: commit.id)
expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
expect(flash[:notice]).to eq('The commit has been successfully reverted.')
end
end
context 'when the revert failed' do
before do
post(:revert,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
target_branch: 'master',
id: commit.id)
end
it 'should redirect to the commit page' do
# Reverting a commit that has been already reverted.
post(:revert,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
target_branch: 'master',
id: commit.id)
expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id)
expect(flash[:alert]).to match('Sorry, we cannot revert this commit automatically.')
end
end
end
describe '#cherry_pick' do
context 'when target branch is not provided' do
it 'should render the 404 page' do
post(:cherry_pick,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: master_pickable_commit.id)
expect(response).not_to be_success
expect(response).to have_http_status(404)
end
end
context 'when the cherry-pick was successful' do
it 'should redirect to the commits page' do
post(:cherry_pick,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
target_branch: 'master',
id: master_pickable_commit.id)
expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
expect(flash[:notice]).to eq('The commit has been successfully cherry-picked.')
end
end
context 'when the cherry_pick failed' do
before do
post(:cherry_pick,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
target_branch: 'master',
id: master_pickable_commit.id)
end
it 'should redirect to the commit page' do
# Cherry-picking a commit that has been already cherry-picked.
post(:cherry_pick,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
target_branch: 'master',
id: master_pickable_commit.id)
expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
expect(flash[:alert]).to match('Sorry, we cannot cherry-pick this commit automatically.')
end
end
end
end

View file

@ -1,9 +1,29 @@
require 'rails_helper' require 'spec_helper'
describe Projects::CommitController do describe Projects::CommitController do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:commit) { project.commit("master") }
let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
let(:master_pickable_commit) { project.commit(master_pickable_sha) }
before do
sign_in(user)
project.team << [user, :master]
end
describe 'GET show' do describe 'GET show' do
render_views render_views
def go(extra_params = {})
params = {
namespace_id: project.namespace.to_param,
project_id: project.to_param
}
get :show, params.merge(extra_params)
end
let(:project) { create(:project) } let(:project) { create(:project) }
before do before do
@ -15,7 +35,7 @@ describe Projects::CommitController do
context 'with valid id' do context 'with valid id' do
it 'responds with 200' do it 'responds with 200' do
go id: project.commit.id go(id: commit.id)
expect(response).to be_ok expect(response).to be_ok
end end
@ -23,27 +43,274 @@ describe Projects::CommitController do
context 'with invalid id' do context 'with invalid id' do
it 'responds with 404' do it 'responds with 404' do
go id: project.commit.id.reverse go(id: commit.id.reverse)
expect(response).to be_not_found expect(response).to be_not_found
end end
end end
it 'handles binary files' do it 'handles binary files' do
get(:show, go(id: TestEnv::BRANCH_SHA['binary-encoding'], format: 'html')
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: TestEnv::BRANCH_SHA['binary-encoding'],
format: "html")
expect(response).to be_success expect(response).to be_success
end end
def go(id:) shared_examples "export as" do |format|
get :show, it "should generally work" do
go(id: commit.id, format: format)
expect(response).to be_success
end
it "should generate it" do
expect_any_instance_of(Commit).to receive(:"to_#{format}")
go(id: commit.id, format: format)
end
it "should render it" do
go(id: commit.id, format: format)
expect(response.body).to eq(commit.send(:"to_#{format}"))
end
it "should not escape Html" do
allow_any_instance_of(Commit).to receive(:"to_#{format}").
and_return('HTML entities &<>" ')
go(id: commit.id, format: format)
expect(response.body).not_to include('&amp;')
expect(response.body).not_to include('&gt;')
expect(response.body).not_to include('&lt;')
expect(response.body).not_to include('&quot;')
end
end
describe "as diff" do
include_examples "export as", :diff
let(:format) { :diff }
it "should really only be a git diff" do
go(id: commit.id, format: format)
expect(response.body).to start_with("diff --git")
end
it "should really only be a git diff without whitespace changes" do
go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format, w: 1)
expect(response.body).to start_with("diff --git")
# without whitespace option, there are more than 2 diff_splits
diff_splits = assigns(:diffs).first.diff.split("\n")
expect(diff_splits.length).to be <= 2
end
end
describe "as patch" do
include_examples "export as", :patch
let(:format) { :patch }
it "should really be a git email patch" do
go(id: commit.id, format: format)
expect(response.body).to start_with("From #{commit.id}")
end
it "should contain a git diff" do
go(id: commit.id, format: format)
expect(response.body).to match(/^diff --git/)
end
end
context 'commit that removes a submodule' do
render_views
let(:fork_project) { create(:forked_project_with_submodules, visibility_level: 20) }
let(:commit) { fork_project.commit('remove-submodule') }
it 'renders it' do
get(:show,
namespace_id: fork_project.namespace.to_param,
project_id: fork_project.to_param,
id: commit.id)
expect(response).to be_success
end
end
end
describe "GET branches" do
it "contains branch and tags information" do
get(:branches,
namespace_id: project.namespace.to_param, namespace_id: project.namespace.to_param,
project_id: project.to_param, project_id: project.to_param,
id: id id: commit.id)
expect(assigns(:branches)).to include("master", "feature_conflict")
expect(assigns(:tags)).to include("v1.1.0")
end
end
describe 'POST revert' do
context 'when target branch is not provided' do
it 'should render the 404 page' do
post(:revert,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: commit.id)
expect(response).not_to be_success
expect(response).to have_http_status(404)
end
end
context 'when the revert was successful' do
it 'should redirect to the commits page' do
post(:revert,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
target_branch: 'master',
id: commit.id)
expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
expect(flash[:notice]).to eq('The commit has been successfully reverted.')
end
end
context 'when the revert failed' do
before do
post(:revert,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
target_branch: 'master',
id: commit.id)
end
it 'should redirect to the commit page' do
# Reverting a commit that has been already reverted.
post(:revert,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
target_branch: 'master',
id: commit.id)
expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id)
expect(flash[:alert]).to match('Sorry, we cannot revert this commit automatically.')
end
end
end
describe 'POST cherry_pick' do
context 'when target branch is not provided' do
it 'should render the 404 page' do
post(:cherry_pick,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: master_pickable_commit.id)
expect(response).not_to be_success
expect(response).to have_http_status(404)
end
end
context 'when the cherry-pick was successful' do
it 'should redirect to the commits page' do
post(:cherry_pick,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
target_branch: 'master',
id: master_pickable_commit.id)
expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
expect(flash[:notice]).to eq('The commit has been successfully cherry-picked.')
end
end
context 'when the cherry_pick failed' do
before do
post(:cherry_pick,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
target_branch: 'master',
id: master_pickable_commit.id)
end
it 'should redirect to the commit page' do
# Cherry-picking a commit that has been already cherry-picked.
post(:cherry_pick,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
target_branch: 'master',
id: master_pickable_commit.id)
expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
expect(flash[:alert]).to match('Sorry, we cannot cherry-pick this commit automatically.')
end
end
end
describe 'GET diff_for_path' do
def diff_for_path(extra_params = {})
params = {
namespace_id: project.namespace.to_param,
project_id: project.to_param
}
get :diff_for_path, params.merge(extra_params)
end
let(:existing_path) { '.gitmodules' }
context 'when the commit exists' do
context 'when the user has access to the project' do
context 'when the path exists in the diff' do
it 'enables diff notes' do
diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
expect(assigns(:diff_notes_disabled)).to be_falsey
expect(assigns(:comments_target)).to eq(noteable_type: 'Commit',
commit_id: commit.id)
end
it 'only renders the diffs for the path given' do
expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project|
expect(diffs.map(&:new_path)).to contain_exactly(existing_path)
meth.call(diffs, diff_refs, project)
end
diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
end
end
context 'when the path does not exist in the diff' do
before { diff_for_path(id: commit.id, old_path: existing_path.succ, new_path: existing_path.succ) }
it 'returns a 404' do
expect(response).to have_http_status(404)
end
end
end
context 'when the user does not have access to the project' do
before do
project.team.truncate
diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
end
it 'returns a 404' do
expect(response).to have_http_status(404)
end
end
end
context 'when the commit does not exist' do
before { diff_for_path(id: commit.id.succ, old_path: existing_path, new_path: existing_path) }
it 'returns a 404' do
expect(response).to have_http_status(404)
end
end end
end end
end end

View file

@ -64,4 +64,73 @@ describe Projects::CompareController do
expect(assigns(:commits)).to eq(nil) expect(assigns(:commits)).to eq(nil)
end end
end end
describe 'GET diff_for_path' do
def diff_for_path(extra_params = {})
params = {
namespace_id: project.namespace.to_param,
project_id: project.to_param
}
get :diff_for_path, params.merge(extra_params)
end
let(:existing_path) { 'files/ruby/feature.rb' }
context 'when the from and to refs exist' do
context 'when the user has access to the project' do
context 'when the path exists in the diff' do
it 'disables diff notes' do
diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
expect(assigns(:diff_notes_disabled)).to be_truthy
end
it 'only renders the diffs for the path given' do
expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project|
expect(diffs.map(&:new_path)).to contain_exactly(existing_path)
meth.call(diffs, diff_refs, project)
end
diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
end
end
context 'when the path does not exist in the diff' do
before { diff_for_path(from: ref_from, to: ref_to, old_path: existing_path.succ, new_path: existing_path.succ) }
it 'returns a 404' do
expect(response).to have_http_status(404)
end
end
end
context 'when the user does not have access to the project' do
before do
project.team.truncate
diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
end
it 'returns a 404' do
expect(response).to have_http_status(404)
end
end
end
context 'when the from ref does not exist' do
before { diff_for_path(from: ref_from.succ, to: ref_to, old_path: existing_path, new_path: existing_path) }
it 'returns a 404' do
expect(response).to have_http_status(404)
end
end
context 'when the to ref does not exist' do
before { diff_for_path(from: ref_from, to: ref_to.succ, old_path: existing_path, new_path: existing_path) }
it 'returns a 404' do
expect(response).to have_http_status(404)
end
end
end
end end

View file

@ -10,7 +10,7 @@ describe Projects::MergeRequestsController do
project.team << [user, :master] project.team << [user, :master]
end end
describe '#new' do describe 'GET new' do
context 'merge request that removes a submodule' do context 'merge request that removes a submodule' do
render_views render_views
@ -34,7 +34,7 @@ describe Projects::MergeRequestsController do
end end
end end
describe "#show" do describe "GET show" do
shared_examples "export merge as" do |format| shared_examples "export merge as" do |format|
it "should generally work" do it "should generally work" do
get(:show, get(:show,
@ -108,7 +108,7 @@ describe Projects::MergeRequestsController do
end end
end end
describe 'GET #index' do describe 'GET index' do
def get_merge_requests def get_merge_requests
get :index, get :index,
namespace_id: project.namespace.to_param, namespace_id: project.namespace.to_param,
@ -140,7 +140,7 @@ describe Projects::MergeRequestsController do
end end
end end
describe 'PUT #update' do describe 'PUT update' do
context 'there is no source project' do context 'there is no source project' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:fork_project) { create(:forked_project_with_submodules) } let(:fork_project) { create(:forked_project_with_submodules) }
@ -168,7 +168,7 @@ describe Projects::MergeRequestsController do
end end
end end
describe 'POST #merge' do describe 'POST merge' do
let(:base_params) do let(:base_params) do
{ {
namespace_id: project.namespace.path, namespace_id: project.namespace.path,
@ -266,7 +266,7 @@ describe Projects::MergeRequestsController do
end end
end end
describe "DELETE #destroy" do describe "DELETE destroy" do
it "denies access to users unless they're admin or project owner" do it "denies access to users unless they're admin or project owner" do
delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
@ -290,26 +290,29 @@ describe Projects::MergeRequestsController do
end end
describe 'GET diffs' do describe 'GET diffs' do
def go(format: 'html') def go(extra_params = {})
get :diffs, params = {
namespace_id: project.namespace.to_param, namespace_id: project.namespace.to_param,
project_id: project.to_param, project_id: project.to_param,
id: merge_request.iid, id: merge_request.iid
format: format }
get :diffs, params.merge(extra_params)
end end
context 'with default params' do
context 'as html' do context 'as html' do
it 'renders the diff template' do before { go(format: 'html') }
go
it 'renders the diff template' do
expect(response).to render_template('diffs') expect(response).to render_template('diffs')
end end
end end
context 'as json' do context 'as json' do
it 'renders the diffs template to a string' do before { go(format: 'json') }
go format: 'json'
it 'renders the diffs template to a string' do
expect(response).to render_template('projects/merge_requests/show/_diffs') expect(response).to render_template('projects/merge_requests/show/_diffs')
expect(JSON.parse(response.body)).to have_key('html') expect(JSON.parse(response.body)).to have_key('html')
end end
@ -326,62 +329,173 @@ describe Projects::MergeRequestsController do
fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
fork_project.save fork_project.save
merge_request.reload merge_request.reload
go(format: 'json')
end end
it 'renders' do it 'renders' do
go format: 'json'
expect(response).to be_success expect(response).to be_success
expect(response.body).to have_content('Subproject commit') expect(response.body).to have_content('Subproject commit')
end end
end end
end end
describe 'GET diffs with ignore_whitespace_change' do context 'with ignore_whitespace_change' do
def go(format: 'html')
get :diffs,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: merge_request.iid,
format: format,
w: 1
end
context 'as html' do context 'as html' do
it 'renders the diff template' do before { go(format: 'html', w: 1) }
go
it 'renders the diff template' do
expect(response).to render_template('diffs') expect(response).to render_template('diffs')
end end
end end
context 'as json' do context 'as json' do
it 'renders the diffs template to a string' do before { go(format: 'json', w: 1) }
go format: 'json'
it 'renders the diffs template to a string' do
expect(response).to render_template('projects/merge_requests/show/_diffs') expect(response).to render_template('projects/merge_requests/show/_diffs')
expect(JSON.parse(response.body)).to have_key('html') expect(JSON.parse(response.body)).to have_key('html')
end end
end end
end end
describe 'GET diffs with view' do context 'with view' do
def go(extra_params = {}) before { go(view: 'parallel') }
params = {
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: merge_request.iid
}
get :diffs, params.merge(extra_params)
end
it 'saves the preferred diff view in a cookie' do it 'saves the preferred diff view in a cookie' do
go view: 'parallel'
expect(response.cookies['diff_view']).to eq('parallel') expect(response.cookies['diff_view']).to eq('parallel')
end end
end end
end
describe 'GET diff_for_path' do
def diff_for_path(extra_params = {})
params = {
namespace_id: project.namespace.to_param,
project_id: project.to_param
}
get :diff_for_path, params.merge(extra_params)
end
context 'when an ID param is passed' do
let(:existing_path) { 'files/ruby/popen.rb' }
context 'when the merge request exists' do
context 'when the user can view the merge request' do
context 'when the path exists in the diff' do
it 'enables diff notes' do
diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path)
expect(assigns(:diff_notes_disabled)).to be_falsey
expect(assigns(:comments_target)).to eq(noteable_type: 'MergeRequest',
noteable_id: merge_request.id)
end
it 'only renders the diffs for the path given' do
expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project|
expect(diffs.map(&:new_path)).to contain_exactly(existing_path)
meth.call(diffs, diff_refs, project)
end
diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path)
end
end
context 'when the path does not exist in the diff' do
before { diff_for_path(id: merge_request.iid, old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb') }
it 'returns a 404' do
expect(response).to have_http_status(404)
end
end
end
context 'when the user cannot view the merge request' do
before do
project.team.truncate
diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path)
end
it 'returns a 404' do
expect(response).to have_http_status(404)
end
end
end
context 'when the merge request does not exist' do
before { diff_for_path(id: merge_request.iid.succ, old_path: existing_path, new_path: existing_path) }
it 'returns a 404' do
expect(response).to have_http_status(404)
end
end
context 'when the merge request belongs to a different project' do
let(:other_project) { create(:empty_project) }
before do
other_project.team << [user, :master]
diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project.to_param)
end
it 'returns a 404' do
expect(response).to have_http_status(404)
end
end
end
context 'when source and target params are passed' do
let(:existing_path) { 'files/ruby/feature.rb' }
context 'when both branches are in the same project' do
it 'disables diff notes' do
diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' })
expect(assigns(:diff_notes_disabled)).to be_truthy
end
it 'only renders the diffs for the path given' do
expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project|
expect(diffs.map(&:new_path)).to contain_exactly(existing_path)
meth.call(diffs, diff_refs, project)
end
diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' })
end
end
context 'when the source branch is in a different project to the target' do
let(:other_project) { create(:project) }
before { other_project.team << [user, :master] }
context 'when the path exists in the diff' do
it 'disables diff notes' do
diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' })
expect(assigns(:diff_notes_disabled)).to be_truthy
end
it 'only renders the diffs for the path given' do
expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project|
expect(diffs.map(&:new_path)).to contain_exactly(existing_path)
meth.call(diffs, diff_refs, project)
end
diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' })
end
end
context 'when the path does not exist in the diff' do
before { diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb', merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) }
it 'returns a 404' do
expect(response).to have_http_status(404)
end
end
end
end
end
describe 'GET commits' do describe 'GET commits' do
def go(format: 'html') def go(format: 'html')

View file

@ -0,0 +1,207 @@
require 'spec_helper'
feature 'Expand and collapse diffs', js: true, feature: true do
include WaitForAjax
before do
login_as :admin
project = create(:project)
branch = 'expand-collapse-diffs'
# Ensure that undiffable.md is in .gitattributes
project.repository.copy_gitattributes(branch)
visit namespace_project_commit_path(project.namespace, project, project.commit(branch))
execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });')
end
def file_container(filename)
find("[data-blob-diff-path*='#{filename}']")
end
# Use define_method instead of let (which is memoized) so that this just works across a
# reload.
#
files = [
'small_diff.md', 'large_diff.md', 'large_diff_renamed.md', 'undiffable.md',
'too_large.md', 'too_large_image.jpg'
]
files.each do |file|
define_method(file.split('.').first) { file_container(file) }
end
context 'visiting a commit with collapsed diffs' do
it 'shows small diffs immediately' do
expect(small_diff).to have_selector('.code')
expect(small_diff).not_to have_selector('.nothing-here-block')
end
it 'collapses large diffs by default' do
expect(large_diff).not_to have_selector('.code')
expect(large_diff).to have_selector('.nothing-here-block')
end
it 'collapses large diffs for renamed files by default' do
expect(large_diff_renamed).not_to have_selector('.code')
expect(large_diff_renamed).to have_selector('.nothing-here-block')
expect(large_diff_renamed).to have_selector('.file-title .deletion')
expect(large_diff_renamed).to have_selector('.file-title .addition')
end
it 'shows non-renderable diffs as such immediately, regardless of their size' do
expect(undiffable).not_to have_selector('.code')
expect(undiffable).to have_selector('.nothing-here-block')
expect(undiffable).to have_content('gitattributes')
end
it 'does not allow diffs that are larger than the maximum size to be expanded' do
expect(too_large).not_to have_selector('.code')
expect(too_large).to have_selector('.nothing-here-block')
expect(too_large).to have_content('too large')
end
it 'shows image diffs immediately, regardless of their size' do
expect(too_large_image).not_to have_selector('.nothing-here-block')
expect(too_large_image).to have_selector('.image')
end
context 'expanding a diff for a renamed file' do
before do
large_diff_renamed.find('.nothing-here-block').click
wait_for_ajax
end
it 'shows the old content' do
old_line = large_diff_renamed.find('.line_content.old')
expect(old_line).to have_content('two copies')
end
it 'shows the new content' do
new_line = large_diff_renamed.find('.line_content.new', match: :prefer_exact)
expect(new_line).to have_content('three copies')
end
end
context 'expanding a large diff' do
before do
click_link('large_diff.md')
wait_for_ajax
end
it 'makes a request to get the content' do
ajax_uris = evaluate_script('ajaxUris')
expect(ajax_uris).not_to be_empty
expect(ajax_uris.first).to include('large_diff.md')
end
it 'shows the diff content' do
expect(large_diff).to have_selector('.code')
expect(large_diff).not_to have_selector('.nothing-here-block')
end
context 'adding a comment to the expanded diff' do
let(:comment_text) { 'A comment' }
before do
large_diff.find('.line_holder', match: :prefer_exact).hover
large_diff.find('.add-diff-note').click
large_diff.find('.note-textarea').send_keys comment_text
large_diff.find_button('Comment').click
wait_for_ajax
end
it 'adds the comment' do
expect(large_diff.find('.notes')).to have_content comment_text
end
context 'reloading the page' do
before { refresh }
it 'collapses the large diff by default' do
expect(large_diff).not_to have_selector('.code')
expect(large_diff).to have_selector('.nothing-here-block')
end
context 'expanding the diff' do
before do
click_link('large_diff.md')
wait_for_ajax
end
it 'shows the diff content' do
expect(large_diff).to have_selector('.code')
expect(large_diff).not_to have_selector('.nothing-here-block')
end
it 'shows the diff comment' do
expect(large_diff.find('.notes')).to have_content comment_text
end
end
end
end
end
context 'collapsing an expanded diff' do
before { click_link('small_diff.md') }
it 'hides the diff content' do
expect(small_diff).not_to have_selector('.code')
expect(small_diff).to have_selector('.nothing-here-block')
end
context 're-expanding the same diff' do
before { click_link('small_diff.md') }
it 'shows the diff content' do
expect(small_diff).to have_selector('.code')
expect(small_diff).not_to have_selector('.nothing-here-block')
end
it 'does not make a new HTTP request' do
expect(evaluate_script('ajaxUris')).to be_empty
end
end
end
end
context 'expanding all diffs' do
before do
click_link('Expand all')
wait_for_ajax
execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });')
end
it 'reloads the page with all diffs expanded' do
expect(small_diff).to have_selector('.code')
expect(small_diff).not_to have_selector('.nothing-here-block')
expect(large_diff).to have_selector('.code')
expect(large_diff).not_to have_selector('.nothing-here-block')
end
context 'collapsing an expanded diff' do
before { click_link('small_diff.md') }
it 'hides the diff content' do
expect(small_diff).not_to have_selector('.code')
expect(small_diff).to have_selector('.nothing-here-block')
end
context 're-expanding the same diff' do
before { click_link('small_diff.md') }
it 'shows the diff content' do
expect(small_diff).to have_selector('.code')
expect(small_diff).not_to have_selector('.nothing-here-block')
end
it 'does not make a new HTTP request' do
expect(evaluate_script('ajaxUris')).to be_empty
end
end
end
end
end

View file

@ -76,7 +76,7 @@ feature 'Prioritize labels', feature: true do
expect(page.all('li').last).to have_content('bug') expect(page.all('li').last).to have_content('bug')
end end
visit current_url refresh
wait_for_ajax wait_for_ajax
page.within('.prioritized-labels') do page.within('.prioritized-labels') do

View file

@ -31,26 +31,11 @@ describe DiffHelper do
end end
end end
describe 'diff_hard_limit_enabled?' do
it 'should return true if param is provided' do
allow(controller).to receive(:params) { { force_show_diff: true } }
expect(diff_hard_limit_enabled?).to be_truthy
end
it 'should return false if param is not provided' do
expect(diff_hard_limit_enabled?).to be_falsey
end
end
describe 'diff_options' do describe 'diff_options' do
it 'should return hard limit for a diff if force diff is true' do it 'should return hard limit for a diff' do
allow(controller).to receive(:params) { { force_show_diff: true } } allow(controller).to receive(:params) { { force_show_diff: true } }
expect(diff_options).to include(Commit.max_diff_options) expect(diff_options).to include(Commit.max_diff_options)
end end
it 'should return safe limit for a diff if force diff is false' do
expect(diff_options).not_to include(:max_lines, :max_files)
end
end end
describe 'unfold_bottom_class' do describe 'unfold_bottom_class' do

View file

@ -0,0 +1,47 @@
require 'spec_helper'
describe MergeRequestDiff, models: true do
describe '#diffs' do
let(:mr) { create(:merge_request, :with_diffs) }
let(:mr_diff) { mr.merge_request_diff }
context 'when the :ignore_whitespace_change option is set' do
it 'creates a new compare object instead of loading from the DB' do
expect(mr_diff).not_to receive(:load_diffs)
expect(Gitlab::Git::Compare).to receive(:new).and_call_original
mr_diff.diffs(ignore_whitespace_change: true)
end
end
context 'when the raw diffs are empty' do
before { mr_diff.update_attributes(st_diffs: '') }
it 'returns an empty DiffCollection' do
expect(mr_diff.diffs).to be_a(Gitlab::Git::DiffCollection)
expect(mr_diff.diffs).to be_empty
end
end
context 'when the raw diffs exist' do
it 'returns the diffs' do
expect(mr_diff.diffs).to be_a(Gitlab::Git::DiffCollection)
expect(mr_diff.diffs).not_to be_empty
end
context 'when the :paths option is set' do
let(:diffs) { mr_diff.diffs(paths: ['files/ruby/popen.rb', 'files/ruby/popen.rb']) }
it 'only returns diffs that match the (old path, new path) given' do
expect(diffs.map(&:new_path)).to contain_exactly('files/ruby/popen.rb')
end
it 'uses the diffs from the DB' do
expect(mr_diff).to receive(:load_diffs)
diffs
end
end
end
end
end

View file

@ -116,6 +116,31 @@ describe MergeRequest, models: true do
end end
end end
describe '#diffs' do
let(:merge_request) { build(:merge_request) }
let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }
context 'when there are MR diffs' do
it 'delegates to the MR diffs' do
merge_request.merge_request_diff = MergeRequestDiff.new
expect(merge_request.merge_request_diff).to receive(:diffs).with(options)
merge_request.diffs(options)
end
end
context 'when there are no MR diffs' do
it 'delegates to the compare object' do
merge_request.compare = double(:compare)
expect(merge_request.compare).to receive(:diffs).with(options)
merge_request.diffs(options)
end
end
end
describe "#mr_and_commit_notes" do describe "#mr_and_commit_notes" do
let!(:merge_request) { create(:merge_request) } let!(:merge_request) { create(:merge_request) }

View file

@ -27,6 +27,14 @@ module CapybaraHelpers
end end
end end
end end
# Refresh the page. Calling `visit current_url` doesn't seem to work consistently.
#
def refresh
url = current_url
visit 'about:blank'
visit url
end
end end
RSpec.configure do |config| RSpec.configure do |config|

View file

@ -18,6 +18,7 @@ module TestEnv
'orphaned-branch' => '45127a9', 'orphaned-branch' => '45127a9',
'binary-encoding' => '7b1cf43', 'binary-encoding' => '7b1cf43',
'gitattributes' => '5a62481', 'gitattributes' => '5a62481',
'expand-collapse-diffs' => '4842455'
} }
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily