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 26 end
27 27 end
28 28  
29   - format.patch
  29 + format.diff { render text: @commit.to_diff }
  30 + format.patch { render text: @commit.to_patch }
30 31 end
31 32 end
32 33 end
... ...
app/controllers/merge_requests_controller.rb
1 1 class MergeRequestsController < ProjectResourceController
2 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 5 before_filter :define_show_vars, only: [:show, :diffs]
6 6  
7 7 # Allow read any merge_request
... ... @@ -16,7 +16,6 @@ class MergeRequestsController &lt; ProjectResourceController
16 16 # Allow destroy merge_request
17 17 before_filter :authorize_admin_merge_request!, only: [:destroy]
18 18  
19   -
20 19 def index
21 20 @merge_requests = MergeRequestsLoadContext.new(project, current_user, params).execute
22 21 end
... ... @@ -25,11 +24,10 @@ class MergeRequestsController &lt; ProjectResourceController
25 24 respond_to do |format|
26 25 format.html
27 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 31 end
34 32  
35 33 def diffs
... ...
app/models/commit.rb
... ... @@ -150,4 +150,19 @@ class Commit
150 150 def parents_count
151 151 parents && parents.count || 0
152 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 168 end
... ...
app/models/merge_request.rb
... ... @@ -202,20 +202,22 @@ class MergeRequest &lt; ActiveRecord::Base
202 202 false
203 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 205 def mr_and_commit_notes
218 206 commit_ids = commits.map(&:id)
219 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 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 223 end
... ...
app/views/commit/show.patch.erb
... ... @@ -1 +0,0 @@
1   -<%= @commit.to_patch %>
app/views/commits/_commit_box.html.haml
... ... @@ -5,9 +5,14 @@
5 5 %span.btn.disabled.grouped
6 6 %i.icon-comment
7 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 16 = link_to project_tree_path(@project, @commit), class: "browse-button primary grouped" do
12 17 %strong Browse Code »
13 18 %h3.commit-title.page_title
... ...
app/views/merge_requests/show/_diffs.html.haml
1 1 - if @merge_request.valid_diffs?
2 2 = render "commits/diffs", diffs: @diffs
3 3 - elsif @merge_request.broken_diffs?
4   - %h4.nothing_here_message
  4 + %h4.nothing_here_message
5 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 9 - else
8 10 %h4.nothing_here_message Nothing to merge
... ...
app/views/merge_requests/show/_mr_title.html.haml
... ... @@ -13,9 +13,14 @@
13 13 = "MERGED"
14 14 - if can?(current_user, :modify_merge_request, @merge_request)
15 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 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 # Mime::Type.register "text/richtext", :rtf
5 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 159 end
160 160 end
161 161  
162   - resources :merge_requests do
  162 + resources :merge_requests, constraints: {id: /\d+/} do
163 163 member do
164 164 get :diffs
165 165 get :automerge
166 166 get :automerge_check
167   - get :raw
168 167 end
169 168  
170 169 collection do
... ...
spec/controllers/commit_controller_spec.rb 0 → 100644
... ... @@ -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 @@
  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 70 closed true
71 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 87 factory :closed_merge_request, traits: [:closed]
  88 + factory :merge_request_with_diffs, traits: [:with_diffs]
74 89 end
75 90  
76 91 factory :note do
... ...
spec/routing/project_routing_spec.rb
... ... @@ -208,7 +208,6 @@ end
208 208 # diffs_project_merge_request GET /:project_id/merge_requests/:id/diffs(.:format) merge_requests#diffs
209 209 # automerge_project_merge_request GET /:project_id/merge_requests/:id/automerge(.:format) merge_requests#automerge
210 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 211 # branch_from_project_merge_requests GET /:project_id/merge_requests/branch_from(.:format) merge_requests#branch_from
213 212 # branch_to_project_merge_requests GET /:project_id/merge_requests/branch_to(.:format) merge_requests#branch_to
214 213 # project_merge_requests GET /:project_id/merge_requests(.:format) merge_requests#index
... ... @@ -231,10 +230,6 @@ describe MergeRequestsController, &quot;routing&quot; do
231 230 get("/gitlabhq/merge_requests/1/automerge_check").should route_to('merge_requests#automerge_check', project_id: 'gitlabhq', id: '1')
232 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 233 it "to #branch_from" do
239 234 get("/gitlabhq/merge_requests/branch_from").should route_to('merge_requests#branch_from', project_id: 'gitlabhq')
240 235 end
... ... @@ -243,6 +238,11 @@ describe MergeRequestsController, &quot;routing&quot; do
243 238 get("/gitlabhq/merge_requests/branch_to").should route_to('merge_requests#branch_to', project_id: 'gitlabhq')
244 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 246 it_behaves_like "RESTful project resources" do
247 247 let(:controller) { 'merge_requests' }
248 248 end
... ... @@ -285,6 +285,7 @@ end
285 285 describe CommitController, "routing" do
286 286 it "to #show" do
287 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 289 get("/gitlabhq/commit/4246fb.patch").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fb', format: 'patch')
289 290 get("/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5").should route_to('commit#show', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5')
290 291 end
... ...