evanphx / gx

A set of git tools

gx / git-update.rb
eff69b62 » evanphx 2008-07-17 Initial version 1 #!/usr/bin/env ruby
2
3 # TODO:
4 # * Handle a conflict in a remote commit that is in a renamed file.
5
6 $:.unshift File.dirname(__FILE__)
7
8 require 'enhance'
9 require 'optparse'
10 require 'ostruct'
11 require 'fileutils'
12 require 'readline'
13
14 opts = OpenStruct.new
15
16 op = OptionParser.new do |o|
17 o.on "-z", "--analyze", "Output information on what update would do" do
18 opts.analyze = true
19 end
20
21 o.on "-v", "--verbose", "Be verbose" do
22 opts.verbose = true
23 end
24
25 o.on "--debug", "Show all git commands run" do
26 Grit.debug = true
27 end
28
29 o.on "-q", "--quiet", "Show the minimal output" do
30 opts.quiet = true
31 end
32 end
33
34 op.parse!(ARGV)
35
36 STDOUT.sync = true
37
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 38 class Update
39 HELP = <<-TXT
eff69b62 » evanphx 2008-07-17 Initial version 40 You're currently inside the conflict resolver. The following commands
41 are available to help you.
42
43 When the conflict resolver is first started, the contents of the file
44 will contain the file populated with conflict markers for you to edit.
45
7b0dae3c » evanphx 2009-04-20 Fix to work with grit 1.1.1 46 [D]iff View the diffs between the (original version and local version)
eff69b62 » evanphx 2008-07-17 Initial version 47 and (original version and remote version).
48 [E]dit Launch your editor to edit the file.
49 [T]ool Run git-mergetool on the file.
50 [O]riginal Set the contents of the file to the original version. This is
51 version from the common ancestor of your commit and the remote
52 commit.
53 [M]ine Set the contents of the file to be your version.
54 [R]emote Set the contents of the file to be the remote version.
55 co[N]flict Set the contents of the file to contain the merged between the
56 local version and remote version, with conflict markers.
57 [P]rompt Launch a subshell to deal with the conflict. Simply exit
58 from the shell to continue with conflict resolution.
59 [I]nfo View information about the commit and the current file.
60 [A]bort Cancel the update all together, restore everything to before
61 the update was started.
62 [C]ontinue You're done dealing with this conflict, move on to the next one.
63 [H]elp Detail all available options, you're looking at it now.
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 64 TXT
eff69b62 » evanphx 2008-07-17 Initial version 65
66
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 67 def initialize(opts)
68 @opts = opts
69 @repo = Grit::Repo.current
e197d849 » evanphx 2009-03-09 Fix patch so 'git am' works... 70
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 71 @current = @repo.resolve_rev "HEAD"
72 @branch = @repo.git.symbolic_ref({:q => true}, "HEAD").strip
73
74 @branch_name = @branch.gsub %r!^refs/heads/!, ""
75
76 @origin_ref = @repo.merge_ref @branch_name
77
78 unless @origin_ref
79 puts "Sorry, it appears you're current branch is not setup with merge info."
80 puts "Please set 'branch.#{@branch_name}.remote' and 'branch.#{@branch_name}.merge'"
81 puts "and try again."
82 exit 1
83 end
84 end
85
86 def fetch
87 print "Fetching new commits: "
7b0dae3c » evanphx 2009-04-20 Fix to work with grit 1.1.1 88 out = @repo.git.fetch :timeout => false
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 89 puts "done."
90
91 # TODO parse +out+ for details to show the user.
92 end
e197d849 » evanphx 2009-03-09 Fix patch so 'git am' works... 93
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 94 def includes_conflict_markers?(path)
95 /^<<<<<<< HEAD/.match(File.read(path))
96 end
eff69b62 » evanphx 2008-07-17 Initial version 97
07ad06aa » evanphx 2009-06-23 Update for newer grit 98 def cat_file(ref, file)
99 File.open(file, "w") do |f|
100 f << @repo.git.cat_file({}, ref)
101 end
102 end
103
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 104 def handle_unmerged(patch_info, files)
105 files.each do |name, info|
106 system "cp #{name} .git/with_markers"
eff69b62 » evanphx 2008-07-17 Initial version 107
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 108 puts
109 puts "Conflict discovered in '#{name}'"
110
111 loop do
eff69b62 » evanphx 2008-07-17 Initial version 112
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 113 # If there are conflict markers, default is edit.
eff69b62 » evanphx 2008-07-17 Initial version 114 if includes_conflict_markers?(name)
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 115 default = "E"
116
117 # otherwise it's continue.
eff69b62 » evanphx 2008-07-17 Initial version 118 else
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 119 default = "C"
120 end
121
122 ans = Readline.readline "Select: [D]iff, [E]dit, [C]ontinue, [H]elp: [#{default}] "
123 ans = default if ans.empty?
124 want = ans.downcase[0]
125 case want
126 when ?d
127 orig = ".git/diff/original/#{name}"
128 FileUtils.mkdir_p File.dirname(orig)
07ad06aa » evanphx 2009-06-23 Update for newer grit 129 cat_file info.original, orig
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 130
131 mine = ".git/diff/mine/#{name}"
132 FileUtils.mkdir_p File.dirname(mine)
07ad06aa » evanphx 2009-06-23 Update for newer grit 133 cat_file info.mine, mine
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 134
135 remote = ".git/diff/remote/#{name}"
136 FileUtils.mkdir_p File.dirname(remote)
07ad06aa » evanphx 2009-06-23 Update for newer grit 137 cat_file info.yours, remote
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 138
139 system "cd .git/diff; diff -u original/#{name} mine/#{name}"
140 system "cd .git/diff; diff -u original/#{name} remote/#{name}"
141 system "rm -rf .git/diff"
142 when ?e
143 system "#{ENV['EDITOR']} #{name}"
144 when ?t
145 system "git mergetool #{name}"
146 when ?o
07ad06aa » evanphx 2009-06-23 Update for newer grit 147 cat_file info.original, name
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 148 when ?m
07ad06aa » evanphx 2009-06-23 Update for newer grit 149 cat_file info.mine, name
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 150 when ?r
07ad06aa » evanphx 2009-06-23 Update for newer grit 151 cat_file info.yours, name
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 152 when ?n
153 system "cp .git/with_markers #{name}"
154 when ?p
155 puts "Starting a sub-shell to handle conflicts for #{name}."
156 puts "Exit the shell to continue resolution."
157 system "$SHELL"
158 when ?i
159 puts "Current file: #{name}"
160 puts "Current commit:"
161 puts " Subject: #{patch_info[:subject]}"
162 puts " Date: #{patch_info[:date]}"
163 puts " Author: #{patch_info[:author]} (#{patch_info[:email]})"
164 when ?a
165 raise "abort!"
166 when ?h
167 puts HELP
168 when ?c
169 if includes_conflict_markers?(name)
170 puts
171 puts "It looks like this file still contains conflict markers."
172 a = Readline.readline "Are you sure that you want to commit it? [Y/N]: "
173 break if a.downcase[0] == ?y
174 else
175 break
176 end
177 else
178 puts "Unknown option. Try again."
eff69b62 » evanphx 2008-07-17 Initial version 179 end
180 end
181
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 182 File.unlink ".git/with_markers" rescue nil
183 @repo.git.add({}, name)
184 end
eff69b62 » evanphx 2008-07-17 Initial version 185 end
186
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 187 def analyze
188 puts "Automatically merging in refs from: #{@origin_ref} / #{@origin[0,7]}"
189 puts "Closest ancestor between HEAD and origin: #{@common[0,7]}"
190 puts
eff69b62 » evanphx 2008-07-17 Initial version 191
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 192 if @to_receive.empty?
193 puts "Current history is up to date."
194 exit 0
195 end
eff69b62 » evanphx 2008-07-17 Initial version 196
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 197 puts "#{@to_receive.size} new commits."
198 if @opts.verbose
199 system "git log --pretty=oneline #{@common}..#{@origin_ref}"
200 puts
201 end
eff69b62 » evanphx 2008-07-17 Initial version 202
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 203 puts "#{@to_replay.size} commits to adapt."
204 if @opts.verbose
205 system "git log --pretty=oneline #{@common}..HEAD"
206 puts
eff69b62 » evanphx 2008-07-17 Initial version 207 end
208 end
209
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 210 def run
211
212 fetch
213
214 @origin = @repo.resolve_rev @origin_ref
215
216 @common = @repo.find_ancestor(@origin, @current)
217
218 @to_replay = @repo.revs_between(@common, @current)
219 @to_receive = @repo.revs_between(@common, @origin)
220
221 if @opts.analyze
222 analyze
223 exit 0
eff69b62 » evanphx 2008-07-17 Initial version 224 end
225
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 226 if @to_receive.empty?
227 puts "Up to date."
228 exit 0
229 end
230
231 if @opts.verbose
232 puts "Extracting commits between #{@common[0,7]} and HEAD..."
233 end
eff69b62 » evanphx 2008-07-17 Initial version 234
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 235 # DANGER. Before here, we can abort anytime, after here, we're making
236 # changes, so we need to be able to recover.
237 #
238 begin
239 port_changes
240 rescue Exception => e
241 puts "Error detected, aborting update: #{e.message} (#{e.class})"
242 puts e.backtrace
243 recover
244 exit 1
245 end
eff69b62 » evanphx 2008-07-17 Initial version 246 end
247
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 248 def recover
249 @repo.git.reset({:hard => true}, @current)
250 @repo.git.checkout({}, @branch.gsub(%r!^refs/heads/!, ""))
251
252 if @used_wip
253 @repo.git.reset({:mixed => true}, "HEAD^")
254 end
255
256 system "rm -rf #{Grit.rebase_dir}" rescue nil
eff69b62 » evanphx 2008-07-17 Initial version 257 end
258
7b0dae3c » evanphx 2009-04-20 Fix to work with grit 1.1.1 259 def sh(cmd)
260 Grit.log cmd if Grit.debug
261 out = `#{cmd}`
262 Grit.log out if Grit.debug
263 end
264
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 265 def port_changes
266 # Switch back in time so we can re-apply commits. checkout
267 # will return non-zero if there it can't be done. In that case
268 # we perform a WIP commit, and unwind that WIP commit later,
269 # leaving the working copy the same way it was.
270
271 @used_wip = false
272
faab1c93 » evanphx 2009-05-25 Check to see if we need to ... 273 list = @repo.git.ls_files(:m => true).split("\n")
274 if list.size > 0
275 @repo.git.commit({:m => "++WIP++", :a => true})
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 276 @used_wip = true
277
278 # Because we've introduced a new commit, we need to repoint current.
279 @current = @repo.resolve_rev "HEAD"
280
281 # And the list of commits to replay.
282 @to_replay = @repo.revs_between(@common, @current)
283
284 # Ok, try again.
7b0dae3c » evanphx 2009-04-20 Fix to work with grit 1.1.1 285 error = @repo.git.checkout({:q => true}, @origin)
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 286 if $?.exitstatus != 0
287 # Ok, give up.
288 recover
289
290 # Now tell the user what happened.
291 puts "ERROR: Sorry, 'git checkout' can't figure out how to properly switch"
292 puts "the working copy. Please fix this and run 'git update' again."
293 puts "Here is the error that 'git checkout' reported:"
294 puts error
295 exit 1
296 end
faab1c93 » evanphx 2009-05-25 Check to see if we need to ... 297 else
298 @repo.git.checkout({:q => true}, @origin)
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 299 end
300
7b0dae3c » evanphx 2009-04-20 Fix to work with grit 1.1.1 301 sh "git format-patch --full-index --stdout #{@common}..#{@current} > .git/update-patch"
302 out = sh "git am --rebasing < .git/update-patch 2> /dev/null"
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 303 while $?.exitstatus != 0
304 info = @repo.am_info
305 if @opts.verbose
306 if info[:subject] == "++WIP++"
307 puts "Conflict detected in working copy."
308 else
309 puts "Conflict detected applying: #{info[:subject]}"
310 end
311 end
eff69b62 » evanphx 2008-07-17 Initial version 312
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 313 unmerged = @repo.unmerged_files
314 handle_unmerged info, unmerged
eff69b62 » evanphx 2008-07-17 Initial version 315
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 316 if @repo.to_be_committed.empty?
317 out = @repo.git.am({:skip => true, "3" => true})
318 else
319 out = @repo.git.am({:resolved => true, "3" => true})
320 end
321 end
eff69b62 » evanphx 2008-07-17 Initial version 322
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 323 # Remove the patch we created contain all the rebased commits
324 File.unlink ".git/update-patch" rescue nil
eff69b62 » evanphx 2008-07-17 Initial version 325
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 326 rev = @repo.resolve_rev "HEAD"
eff69b62 » evanphx 2008-07-17 Initial version 327
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 328 # Update the branch ref to point to our new commit
eff69b62 » evanphx 2008-07-17 Initial version 329
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 330 @repo.git.update_ref({:m => "updated"}, @branch, rev, @current)
331 @repo.git.symbolic_ref({}, "HEAD", @branch)
332
333 # If we inserted a WIP commit on the top, remove the commit, but leave
334 # the work.
335 if @used_wip
336 @repo.git.reset({:mixed => true}, "HEAD^")
337 end
338
339 puts
340 puts "Updated. Imported #{@to_receive.size} commits, HEAD now pointed to #{rev[0,7]}."
341 puts
342
343 unless @opts.quiet
344 system "git diff --stat #{@common}..#{@origin}"
345 end
346
347 end
eff69b62 » evanphx 2008-07-17 Initial version 348 end
92b88d48 » evanphx 2008-07-29 Refactor into class, better... 349
350 Update.new(opts).run