Commit e750efd9fc5b50fc8bed0bc73ba121c03c8b58a4

Authored by Dmitriy Zaporozhets
2 parents 2d5a6fc8 79a4ed15

Merge pull request #2067 from riyad/diff-and-patch-for-commits-and-merge-requests

Diff and patch for commits and merge requests
app/controllers/commit_controller.rb
@@ -26,7 +26,8 @@ class CommitController < ProjectResourceController @@ -26,7 +26,8 @@ class CommitController < ProjectResourceController
26 end 26 end
27 end 27 end
28 28
29 - format.patch 29 + format.diff { render text: @commit.to_diff }
  30 + format.patch { render text: @commit.to_patch }
30 end 31 end
31 end 32 end
32 end 33 end
app/controllers/merge_requests_controller.rb
1 class MergeRequestsController < ProjectResourceController 1 class MergeRequestsController < ProjectResourceController
2 before_filter :module_enabled 2 before_filter :module_enabled
3 - before_filter :merge_request, only: [:edit, :update, :destroy, :show, :commits, :diffs, :automerge, :automerge_check, :raw]  
4 - before_filter :validates_merge_request, only: [:show, :diffs, :raw] 3 + before_filter :merge_request, only: [:edit, :update, :destroy, :show, :commits, :diffs, :automerge, :automerge_check]
  4 + before_filter :validates_merge_request, only: [:show, :diffs]
5 before_filter :define_show_vars, only: [:show, :diffs] 5 before_filter :define_show_vars, only: [:show, :diffs]
6 6
7 # Allow read any merge_request 7 # Allow read any merge_request
@@ -16,7 +16,6 @@ class MergeRequestsController &lt; ProjectResourceController @@ -16,7 +16,6 @@ class MergeRequestsController &lt; ProjectResourceController
16 # Allow destroy merge_request 16 # Allow destroy merge_request
17 before_filter :authorize_admin_merge_request!, only: [:destroy] 17 before_filter :authorize_admin_merge_request!, only: [:destroy]
18 18
19 -  
20 def index 19 def index
21 @merge_requests = MergeRequestsLoadContext.new(project, current_user, params).execute 20 @merge_requests = MergeRequestsLoadContext.new(project, current_user, params).execute
22 end 21 end
@@ -25,11 +24,10 @@ class MergeRequestsController &lt; ProjectResourceController @@ -25,11 +24,10 @@ class MergeRequestsController &lt; ProjectResourceController
25 respond_to do |format| 24 respond_to do |format|
26 format.html 25 format.html
27 format.js 26 format.js
28 - end  
29 - end  
30 27
31 - def raw  
32 - send_file @merge_request.to_raw 28 + format.diff { render text: @merge_request.to_diff }
  29 + format.patch { render text: @merge_request.to_patch }
  30 + end
33 end 31 end
34 32
35 def diffs 33 def diffs
app/models/commit.rb
@@ -150,4 +150,19 @@ class Commit @@ -150,4 +150,19 @@ class Commit
150 def parents_count 150 def parents_count
151 parents && parents.count || 0 151 parents && parents.count || 0
152 end 152 end
  153 +
  154 + # Shows the diff between the commit's parent and the commit.
  155 + #
  156 + # Cuts out the header and stats from #to_patch and returns only the diff.
  157 + def to_diff
  158 + # see Grit::Commit#show
  159 + patch = to_patch
  160 +
  161 + # discard lines before the diff
  162 + lines = patch.split("\n")
  163 + while !lines.first.start_with?("diff --git") do
  164 + lines.shift
  165 + end
  166 + lines.join("\n")
  167 + end
153 end 168 end
app/models/merge_request.rb
@@ -202,20 +202,22 @@ class MergeRequest &lt; ActiveRecord::Base @@ -202,20 +202,22 @@ class MergeRequest &lt; ActiveRecord::Base
202 false 202 false
203 end 203 end
204 204
205 - def to_raw  
206 - FileUtils.mkdir_p(Rails.root.join("tmp", "patches"))  
207 - patch_path = Rails.root.join("tmp", "patches", "merge_request_#{self.id}.patch")  
208 -  
209 - from = commits.last.id  
210 - to = source_branch  
211 -  
212 - project.repo.git.run('', "format-patch" , " > #{patch_path.to_s}", {}, ["#{from}..#{to}", "--stdout"])  
213 -  
214 - patch_path  
215 - end  
216 -  
217 def mr_and_commit_notes 205 def mr_and_commit_notes
218 commit_ids = commits.map(&:id) 206 commit_ids = commits.map(&:id)
219 Note.where("(noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR (noteable_type = 'Commit' AND noteable_id IN (:commit_ids))", mr_id: id, commit_ids: commit_ids) 207 Note.where("(noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR (noteable_type = 'Commit' AND noteable_id IN (:commit_ids))", mr_id: id, commit_ids: commit_ids)
220 end 208 end
  209 +
  210 + # Returns the raw diff for this merge request
  211 + #
  212 + # see "git diff"
  213 + def to_diff
  214 + project.repo.git.native(:diff, {timeout: 30, raise: true}, "#{target_branch}...#{source_branch}")
  215 + end
  216 +
  217 + # Returns the commit as a series of email patches.
  218 + #
  219 + # see "git format-patch"
  220 + def to_patch
  221 + project.repo.git.format_patch({timeout: 30, raise: true, stdout: true}, "#{target_branch}..#{source_branch}")
  222 + end
221 end 223 end
app/views/commit/show.patch.erb
@@ -1 +0,0 @@ @@ -1 +0,0 @@
1 -<%= @commit.to_patch %>  
app/views/commits/_commit_box.html.haml
@@ -5,9 +5,14 @@ @@ -5,9 +5,14 @@
5 %span.btn.disabled.grouped 5 %span.btn.disabled.grouped
6 %i.icon-comment 6 %i.icon-comment
7 = @notes_count 7 = @notes_count
8 - = link_to project_commit_path(@project, @commit, format: :patch), class: "btn small grouped" do  
9 - %i.icon-download-alt  
10 - Get Patch 8 + .left.btn-group
  9 + %a.btn.small.grouped.dropdown-toggle{ data: {toggle: :dropdown} }
  10 + %i.icon-download-alt
  11 + Download as
  12 + %span.caret
  13 + %ul.dropdown-menu
  14 + %li= link_to "Email Patches", project_commit_path(@project, @commit, format: :patch)
  15 + %li= link_to "Plain Diff", project_commit_path(@project, @commit, format: :diff)
11 = link_to project_tree_path(@project, @commit), class: "browse-button primary grouped" do 16 = link_to project_tree_path(@project, @commit), class: "browse-button primary grouped" do
12 %strong Browse Code » 17 %strong Browse Code »
13 %h3.commit-title.page_title 18 %h3.commit-title.page_title
app/views/merge_requests/show/_diffs.html.haml
1 - if @merge_request.valid_diffs? 1 - if @merge_request.valid_diffs?
2 = render "commits/diffs", diffs: @diffs 2 = render "commits/diffs", diffs: @diffs
3 - elsif @merge_request.broken_diffs? 3 - elsif @merge_request.broken_diffs?
4 - %h4.nothing_here_message 4 + %h4.nothing_here_message
5 Can't load diff. 5 Can't load diff.
6 - You can #{link_to "download MR patch", raw_project_merge_request_path(@project, @merge_request), class: "vlink"} instead. 6 + You can
  7 + = link_to "download it", project_merge_request_path(@project, @merge_request), format: :diff, class: "vlink"
  8 + instead.
7 - else 9 - else
8 %h4.nothing_here_message Nothing to merge 10 %h4.nothing_here_message Nothing to merge
app/views/merge_requests/show/_mr_title.html.haml
@@ -13,9 +13,14 @@ @@ -13,9 +13,14 @@
13 = "MERGED" 13 = "MERGED"
14 - if can?(current_user, :modify_merge_request, @merge_request) 14 - if can?(current_user, :modify_merge_request, @merge_request)
15 - if @merge_request.open? 15 - if @merge_request.open?
16 - = link_to raw_project_merge_request_path(@project, @merge_request), class: "btn grouped" do  
17 - %i.icon-download-alt  
18 - Get Patch 16 + .left.btn-group
  17 + %a.btn.grouped.dropdown-toggle{ data: {toggle: :dropdown} }
  18 + %i.icon-download-alt
  19 + Download as
  20 + %span.caret
  21 + %ul.dropdown-menu
  22 + %li= link_to "Email Patches", project_merge_request_path(@project, @merge_request, format: :patch)
  23 + %li= link_to "Plain Diff", project_merge_request_path(@project, @merge_request, format: :diff)
19 24
20 = link_to 'Close', project_merge_request_path(@project, @merge_request, merge_request: {closed: true }, status_only: true), method: :put, class: "btn grouped danger", title: "Close merge request" 25 = link_to 'Close', project_merge_request_path(@project, @merge_request, merge_request: {closed: true }, status_only: true), method: :put, class: "btn grouped danger", title: "Close merge request"
21 26
config/initializers/mime_types.rb
@@ -4,4 +4,5 @@ @@ -4,4 +4,5 @@
4 # Mime::Type.register "text/richtext", :rtf 4 # Mime::Type.register "text/richtext", :rtf
5 # Mime::Type.register_alias "text/html", :iphone 5 # Mime::Type.register_alias "text/html", :iphone
6 6
7 -Mime::Type.register_alias 'text/plain', :patch 7 +Mime::Type.register_alias "text/plain", :diff
  8 +Mime::Type.register_alias "text/plain", :patch
config/routes.rb
@@ -159,12 +159,11 @@ Gitlab::Application.routes.draw do @@ -159,12 +159,11 @@ Gitlab::Application.routes.draw do
159 end 159 end
160 end 160 end
161 161
162 - resources :merge_requests do 162 + resources :merge_requests, constraints: {id: /\d+/} do
163 member do 163 member do
164 get :diffs 164 get :diffs
165 get :automerge 165 get :automerge
166 get :automerge_check 166 get :automerge_check
167 - get :raw  
168 end 167 end
169 168
170 collection do 169 collection do
spec/controllers/commit_controller_spec.rb 0 → 100644
@@ -0,0 +1,74 @@ @@ -0,0 +1,74 @@
  1 +require 'spec_helper'
  2 +
  3 +describe CommitController do
  4 + let(:project) { create(:project) }
  5 + let(:user) { create(:user) }
  6 + let(:commit) { project.last_commit_for("master") }
  7 +
  8 + before do
  9 + sign_in(user)
  10 +
  11 + project.add_access(user, :read, :admin)
  12 + end
  13 +
  14 + describe "#show" do
  15 + shared_examples "export as" do |format|
  16 + it "should generally work" do
  17 + get :show, project_id: project.code, id: commit.id, format: format
  18 +
  19 + expect(response).to be_success
  20 + end
  21 +
  22 + it "should generate it" do
  23 + Commit.any_instance.should_receive(:"to_#{format}")
  24 +
  25 + get :show, project_id: project.code, id: commit.id, format: format
  26 + end
  27 +
  28 + it "should render it" do
  29 + get :show, project_id: project.code, id: commit.id, format: format
  30 +
  31 + expect(response.body).to eq(commit.send(:"to_#{format}"))
  32 + end
  33 +
  34 + it "should not escape Html" do
  35 + Commit.any_instance.stub(:"to_#{format}").and_return('HTML entities &<>" ')
  36 +
  37 + get :show, project_id: project.code, id: commit.id, format: format
  38 +
  39 + expect(response.body).to_not include('&amp;')
  40 + expect(response.body).to_not include('&gt;')
  41 + expect(response.body).to_not include('&lt;')
  42 + expect(response.body).to_not include('&quot;')
  43 + end
  44 + end
  45 +
  46 + describe "as diff" do
  47 + include_examples "export as", :diff
  48 + let(:format) { :diff }
  49 +
  50 + it "should really only be a git diff" do
  51 + get :show, project_id: project.code, id: commit.id, format: format
  52 +
  53 + expect(response.body).to start_with("diff --git")
  54 + end
  55 + end
  56 +
  57 + describe "as patch" do
  58 + include_examples "export as", :patch
  59 + let(:format) { :patch }
  60 +
  61 + it "should really be a git email patch" do
  62 + get :show, project_id: project.code, id: commit.id, format: format
  63 +
  64 + expect(response.body).to start_with("From #{commit.id}")
  65 + end
  66 +
  67 + it "should contain a git diff" do
  68 + get :show, project_id: project.code, id: commit.id, format: format
  69 +
  70 + expect(response.body).to match(/^diff --git/)
  71 + end
  72 + end
  73 + end
  74 +end
spec/controllers/merge_requests_controller_spec.rb 0 → 100644
@@ -0,0 +1,84 @@ @@ -0,0 +1,84 @@
  1 +require 'spec_helper'
  2 +
  3 +describe MergeRequestsController do
  4 + let(:project) { create(:project) }
  5 + let(:user) { create(:user) }
  6 + let(:merge_request) { create(:merge_request_with_diffs, project: project) }
  7 +
  8 + before do
  9 + sign_in(user)
  10 +
  11 + project.add_access(user, :read, :admin)
  12 + end
  13 +
  14 + describe "#show" do
  15 + shared_examples "export as" do |format|
  16 + it "should generally work" do
  17 + get :show, project_id: project.code, id: merge_request.id, format: format
  18 +
  19 + expect(response).to be_success
  20 + end
  21 +
  22 + it "should generate it" do
  23 + MergeRequest.any_instance.should_receive(:"to_#{format}")
  24 +
  25 + get :show, project_id: project.code, id: merge_request.id, format: format
  26 + end
  27 +
  28 + it "should render it" do
  29 + get :show, project_id: project.code, id: merge_request.id, format: format
  30 +
  31 + expect(response.body).to eq(merge_request.send(:"to_#{format}"))
  32 + end
  33 +
  34 + it "should not escape Html" do
  35 + MergeRequest.any_instance.stub(:"to_#{format}").and_return('HTML entities &<>" ')
  36 +
  37 + get :show, project_id: project.code, id: merge_request.id, format: format
  38 +
  39 + expect(response.body).to_not include('&amp;')
  40 + expect(response.body).to_not include('&gt;')
  41 + expect(response.body).to_not include('&lt;')
  42 + expect(response.body).to_not include('&quot;')
  43 + end
  44 + end
  45 +
  46 + describe "as diff" do
  47 + include_examples "export as", :diff
  48 + let(:format) { :diff }
  49 +
  50 + it "should really only be a git diff" do
  51 + get :show, project_id: project.code, id: merge_request.id, format: format
  52 +
  53 + expect(response.body).to start_with("diff --git")
  54 + end
  55 + end
  56 +
  57 + describe "as patch" do
  58 + include_examples "export as", :patch
  59 + let(:format) { :patch }
  60 +
  61 + it "should really be a git email patch with commit" do
  62 + get :show, project_id: project.code, id: merge_request.id, format: format
  63 +
  64 + expect(response.body[0..100]).to start_with("From #{merge_request.commits.last.id}")
  65 + end
  66 +
  67 + it "should contain as many patches as there are commits" do
  68 + get :show, project_id: project.code, id: merge_request.id, format: format
  69 +
  70 + patch_count = merge_request.commits.count
  71 + merge_request.commits.each_with_index do |commit, patch_num|
  72 + expect(response.body).to match(/^From #{commit.id}/)
  73 + expect(response.body).to match(/^Subject: \[PATCH #{patch_num}\/#{patch_count}\]/)
  74 + end
  75 + end
  76 +
  77 + it "should contain git diffs" do
  78 + get :show, project_id: project.code, id: merge_request.id, format: format
  79 +
  80 + expect(response.body).to match(/^diff --git/)
  81 + end
  82 + end
  83 + end
  84 +end
spec/factories.rb
@@ -70,7 +70,22 @@ FactoryGirl.define do @@ -70,7 +70,22 @@ FactoryGirl.define do
70 closed true 70 closed true
71 end 71 end
72 72
  73 + # pick 3 commits "at random" (from bcf03b5d~3 to bcf03b5d)
  74 + trait :with_diffs do
  75 + target_branch "bcf03b5d~3"
  76 + source_branch "bcf03b5d"
  77 + st_commits do
  78 + [Commit.new(project.repo.commit('bcf03b5d')),
  79 + Commit.new(project.repo.commit('bcf03b5d~1')),
  80 + Commit.new(project.repo.commit('bcf03b5d~2'))]
  81 + end
  82 + st_diffs do
  83 + project.repo.diff("bcf03b5d~3", "bcf03b5d")
  84 + end
  85 + end
  86 +
73 factory :closed_merge_request, traits: [:closed] 87 factory :closed_merge_request, traits: [:closed]
  88 + factory :merge_request_with_diffs, traits: [:with_diffs]
74 end 89 end
75 90
76 factory :note do 91 factory :note do
spec/routing/project_routing_spec.rb
@@ -208,7 +208,6 @@ end @@ -208,7 +208,6 @@ end
208 # diffs_project_merge_request GET /:project_id/merge_requests/:id/diffs(.:format) merge_requests#diffs 208 # diffs_project_merge_request GET /:project_id/merge_requests/:id/diffs(.:format) merge_requests#diffs
209 # automerge_project_merge_request GET /:project_id/merge_requests/:id/automerge(.:format) merge_requests#automerge 209 # automerge_project_merge_request GET /:project_id/merge_requests/:id/automerge(.:format) merge_requests#automerge
210 # automerge_check_project_merge_request GET /:project_id/merge_requests/:id/automerge_check(.:format) merge_requests#automerge_check 210 # automerge_check_project_merge_request GET /:project_id/merge_requests/:id/automerge_check(.:format) merge_requests#automerge_check
211 -# raw_project_merge_request GET /:project_id/merge_requests/:id/raw(.:format) merge_requests#raw  
212 # branch_from_project_merge_requests GET /:project_id/merge_requests/branch_from(.:format) merge_requests#branch_from 211 # branch_from_project_merge_requests GET /:project_id/merge_requests/branch_from(.:format) merge_requests#branch_from
213 # branch_to_project_merge_requests GET /:project_id/merge_requests/branch_to(.:format) merge_requests#branch_to 212 # branch_to_project_merge_requests GET /:project_id/merge_requests/branch_to(.:format) merge_requests#branch_to
214 # project_merge_requests GET /:project_id/merge_requests(.:format) merge_requests#index 213 # project_merge_requests GET /:project_id/merge_requests(.:format) merge_requests#index
@@ -231,10 +230,6 @@ describe MergeRequestsController, &quot;routing&quot; do @@ -231,10 +230,6 @@ describe MergeRequestsController, &quot;routing&quot; do
231 get("/gitlabhq/merge_requests/1/automerge_check").should route_to('merge_requests#automerge_check', project_id: 'gitlabhq', id: '1') 230 get("/gitlabhq/merge_requests/1/automerge_check").should route_to('merge_requests#automerge_check', project_id: 'gitlabhq', id: '1')
232 end 231 end
233 232
234 - it "to #raw" do  
235 - get("/gitlabhq/merge_requests/1/raw").should route_to('merge_requests#raw', project_id: 'gitlabhq', id: '1')  
236 - end  
237 -  
238 it "to #branch_from" do 233 it "to #branch_from" do
239 get("/gitlabhq/merge_requests/branch_from").should route_to('merge_requests#branch_from', project_id: 'gitlabhq') 234 get("/gitlabhq/merge_requests/branch_from").should route_to('merge_requests#branch_from', project_id: 'gitlabhq')
240 end 235 end
@@ -243,6 +238,11 @@ describe MergeRequestsController, &quot;routing&quot; do @@ -243,6 +238,11 @@ describe MergeRequestsController, &quot;routing&quot; do
243 get("/gitlabhq/merge_requests/branch_to").should route_to('merge_requests#branch_to', project_id: 'gitlabhq') 238 get("/gitlabhq/merge_requests/branch_to").should route_to('merge_requests#branch_to', project_id: 'gitlabhq')
244 end 239 end
245 240
  241 + it "to #show" do
  242 + get("/gitlabhq/merge_requests/1.diff").should route_to('merge_requests#show', project_id: 'gitlabhq', id: '1', format: 'diff')
  243 + get("/gitlabhq/merge_requests/1.patch").should route_to('merge_requests#show', project_id: 'gitlabhq', id: '1', format: 'patch')
  244 + end
  245 +
246 it_behaves_like "RESTful project resources" do 246 it_behaves_like "RESTful project resources" do
247 let(:controller) { 'merge_requests' } 247 let(:controller) { 'merge_requests' }
248 end 248 end
@@ -285,6 +285,7 @@ end @@ -285,6 +285,7 @@ end
285 describe CommitController, "routing" do 285 describe CommitController, "routing" do
286 it "to #show" do 286 it "to #show" do
287 get("/gitlabhq/commit/4246fb").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fb') 287 get("/gitlabhq/commit/4246fb").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fb')
  288 + get("/gitlabhq/commit/4246fb.diff").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fb', format: 'diff')
288 get("/gitlabhq/commit/4246fb.patch").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fb', format: 'patch') 289 get("/gitlabhq/commit/4246fb.patch").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fb', format: 'patch')
289 get("/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') 290 get("/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5')
290 end 291 end