evanphx / gx
- Source
- Commits
- Network (0)
- Issues (0)
- Downloads (0)
- Wiki (1)
- Graphs
-
Branch:
master
gx / git-update.rb
| eff69b62 » | evanphx | 2008-07-17 | 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 | 38 | class Update | |
| 39 | HELP = <<-TXT | ||||
| eff69b62 » | evanphx | 2008-07-17 | 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 | 46 | [D]iff View the diffs between the (original version and local version) | |
| eff69b62 » | evanphx | 2008-07-17 | 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 | 64 | TXT | |
| eff69b62 » | evanphx | 2008-07-17 | 65 | ||
| 66 | |||||
| 92b88d48 » | evanphx | 2008-07-29 | 67 | def initialize(opts) | |
| 68 | @opts = opts | ||||
| 69 | @repo = Grit::Repo.current | ||||
| e197d849 » | evanphx | 2009-03-09 | 70 | ||
| 92b88d48 » | evanphx | 2008-07-29 | 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 | 88 | out = @repo.git.fetch :timeout => false | |
| 92b88d48 » | evanphx | 2008-07-29 | 89 | puts "done." | |
| 90 | |||||
| 91 | # TODO parse +out+ for details to show the user. | ||||
| 92 | end | ||||
| e197d849 » | evanphx | 2009-03-09 | 93 | ||
| 92b88d48 » | evanphx | 2008-07-29 | 94 | def includes_conflict_markers?(path) | |
| 95 | /^<<<<<<< HEAD/.match(File.read(path)) | ||||
| 96 | end | ||||
| eff69b62 » | evanphx | 2008-07-17 | 97 | ||
| 07ad06aa » | evanphx | 2009-06-23 | 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 | 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 | 107 | ||
| 92b88d48 » | evanphx | 2008-07-29 | 108 | puts | |
| 109 | puts "Conflict discovered in '#{name}'" | ||||
| 110 | |||||
| 111 | loop do | ||||
| eff69b62 » | evanphx | 2008-07-17 | 112 | ||
| 92b88d48 » | evanphx | 2008-07-29 | 113 | # If there are conflict markers, default is edit. | |
| eff69b62 » | evanphx | 2008-07-17 | 114 | if includes_conflict_markers?(name) | |
| 92b88d48 » | evanphx | 2008-07-29 | 115 | default = "E" | |
| 116 | |||||
| 117 | # otherwise it's continue. | ||||
| eff69b62 » | evanphx | 2008-07-17 | 118 | else | |
| 92b88d48 » | evanphx | 2008-07-29 | 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 | 129 | cat_file info.original, orig | |
| 92b88d48 » | evanphx | 2008-07-29 | 130 | ||
| 131 | mine = ".git/diff/mine/#{name}" | ||||
| 132 | FileUtils.mkdir_p File.dirname(mine) | ||||
| 07ad06aa » | evanphx | 2009-06-23 | 133 | cat_file info.mine, mine | |
| 92b88d48 » | evanphx | 2008-07-29 | 134 | ||
| 135 | remote = ".git/diff/remote/#{name}" | ||||
| 136 | FileUtils.mkdir_p File.dirname(remote) | ||||
| 07ad06aa » | evanphx | 2009-06-23 | 137 | cat_file info.yours, remote | |
| 92b88d48 » | evanphx | 2008-07-29 | 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 | 147 | cat_file info.original, name | |
| 92b88d48 » | evanphx | 2008-07-29 | 148 | when ?m | |
| 07ad06aa » | evanphx | 2009-06-23 | 149 | cat_file info.mine, name | |
| 92b88d48 » | evanphx | 2008-07-29 | 150 | when ?r | |
| 07ad06aa » | evanphx | 2009-06-23 | 151 | cat_file info.yours, name | |
| 92b88d48 » | evanphx | 2008-07-29 | 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 | 179 | end | |
| 180 | end | ||||
| 181 | |||||
| 92b88d48 » | evanphx | 2008-07-29 | 182 | File.unlink ".git/with_markers" rescue nil | |
| 183 | @repo.git.add({}, name) | ||||
| 184 | end | ||||
| eff69b62 » | evanphx | 2008-07-17 | 185 | end | |
| 186 | |||||
| 92b88d48 » | evanphx | 2008-07-29 | 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 | 191 | ||
| 92b88d48 » | evanphx | 2008-07-29 | 192 | if @to_receive.empty? | |
| 193 | puts "Current history is up to date." | ||||
| 194 | exit 0 | ||||
| 195 | end | ||||
| eff69b62 » | evanphx | 2008-07-17 | 196 | ||
| 92b88d48 » | evanphx | 2008-07-29 | 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 | 202 | ||
| 92b88d48 » | evanphx | 2008-07-29 | 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 | 207 | end | |
| 208 | end | ||||
| 209 | |||||
| 92b88d48 » | evanphx | 2008-07-29 | 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 | 224 | end | |
| 225 | |||||
| 92b88d48 » | evanphx | 2008-07-29 | 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 | 234 | ||
| 92b88d48 » | evanphx | 2008-07-29 | 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 | 246 | end | |
| 247 | |||||
| 92b88d48 » | evanphx | 2008-07-29 | 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 | 257 | end | |
| 258 | |||||
| 7b0dae3c » | evanphx | 2009-04-20 | 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 | 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 | 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 | 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 | 285 | error = @repo.git.checkout({:q => true}, @origin) | |
| 92b88d48 » | evanphx | 2008-07-29 | 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 | 297 | else | |
| 298 | @repo.git.checkout({:q => true}, @origin) | ||||
| 92b88d48 » | evanphx | 2008-07-29 | 299 | end | |
| 300 | |||||
| 7b0dae3c » | evanphx | 2009-04-20 | 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 | 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 | 312 | ||
| 92b88d48 » | evanphx | 2008-07-29 | 313 | unmerged = @repo.unmerged_files | |
| 314 | handle_unmerged info, unmerged | ||||
| eff69b62 » | evanphx | 2008-07-17 | 315 | ||
| 92b88d48 » | evanphx | 2008-07-29 | 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 | 322 | ||
| 92b88d48 » | evanphx | 2008-07-29 | 323 | # Remove the patch we created contain all the rebased commits | |
| 324 | File.unlink ".git/update-patch" rescue nil | ||||
| eff69b62 » | evanphx | 2008-07-17 | 325 | ||
| 92b88d48 » | evanphx | 2008-07-29 | 326 | rev = @repo.resolve_rev "HEAD" | |
| eff69b62 » | evanphx | 2008-07-17 | 327 | ||
| 92b88d48 » | evanphx | 2008-07-29 | 328 | # Update the branch ref to point to our new commit | |
| eff69b62 » | evanphx | 2008-07-17 | 329 | ||
| 92b88d48 » | evanphx | 2008-07-29 | 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 | 348 | end | |
| 92b88d48 » | evanphx | 2008-07-29 | 349 | ||
| 350 | Update.new(opts).run | ||||
