OLD | NEW |
1 # Copyright (c) 2009 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2009 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Gclient-specific SCM-specific operations.""" | 5 """Gclient-specific SCM-specific operations.""" |
6 | 6 |
7 import logging | 7 import logging |
8 import os | 8 import os |
9 import posixpath | 9 import posixpath |
10 import re | 10 import re |
(...skipping 149 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
160 | 160 |
161 Raises: | 161 Raises: |
162 Error: if can't get URL for relative path. | 162 Error: if can't get URL for relative path. |
163 """ | 163 """ |
164 | 164 |
165 if args: | 165 if args: |
166 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args)) | 166 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args)) |
167 | 167 |
168 self._CheckMinVersion("1.6") | 168 self._CheckMinVersion("1.6") |
169 | 169 |
| 170 default_rev = "refs/heads/master" |
170 url, revision = gclient_utils.SplitUrlRevision(self.url) | 171 url, revision = gclient_utils.SplitUrlRevision(self.url) |
171 rev_str = "" | 172 rev_str = "" |
172 if options.revision: | 173 if options.revision: |
173 # Override the revision number. | 174 # Override the revision number. |
174 revision = str(options.revision) | 175 revision = str(options.revision) |
175 if revision: | 176 if not revision: |
176 rev_str = ' at %s' % revision | 177 revision = default_rev |
177 | 178 |
| 179 rev_str = ' at %s' % revision |
| 180 files = [] |
| 181 |
| 182 printed_path = False |
| 183 verbose = [] |
178 if options.verbose: | 184 if options.verbose: |
179 print("\n_____ %s%s" % (self.relpath, rev_str)) | 185 print("\n_____ %s%s" % (self.relpath, rev_str)) |
| 186 verbose = ['--verbose'] |
| 187 printed_path = True |
| 188 |
| 189 if revision.startswith('refs/heads/'): |
| 190 rev_type = "branch" |
| 191 elif revision.startswith('origin/'): |
| 192 # For compatability with old naming, translate 'origin' to 'refs/heads' |
| 193 revision = revision.replace('origin/', 'refs/heads/') |
| 194 rev_type = "branch" |
| 195 else: |
| 196 # hash is also a tag, only make a distinction at checkout |
| 197 rev_type = "hash" |
| 198 |
180 | 199 |
181 if not os.path.exists(self.checkout_path): | 200 if not os.path.exists(self.checkout_path): |
182 # Cloning | 201 self._Clone(rev_type, revision, url, options.verbose) |
183 for i in range(3): | |
184 try: | |
185 self._Run(['clone', url, self.checkout_path], | |
186 cwd=self._root_dir, redirect_stdout=False) | |
187 break | |
188 except gclient_utils.Error, e: | |
189 # TODO(maruel): Hackish, should be fixed by moving _Run() to | |
190 # CheckCall(). | |
191 # Too bad we don't have access to the actual output. | |
192 # We should check for "transfer closed with NNN bytes remaining to | |
193 # read". In the meantime, just make sure .git exists. | |
194 if (e.args[0] == 'git command clone returned 128' and | |
195 os.path.exists(os.path.join(self.checkout_path, '.git'))): | |
196 print str(e) | |
197 print "Retrying..." | |
198 continue | |
199 raise e | |
200 | |
201 if revision: | |
202 self._Run(['reset', '--hard', revision], redirect_stdout=False) | |
203 files = self._Run(['ls-files']).split() | 202 files = self._Run(['ls-files']).split() |
204 file_list.extend([os.path.join(self.checkout_path, f) for f in files]) | 203 file_list.extend([os.path.join(self.checkout_path, f) for f in files]) |
| 204 if not verbose: |
| 205 # Make the output a little prettier. It's nice to have some whitespace |
| 206 # between projects when cloning. |
| 207 print "" |
205 return | 208 return |
206 | 209 |
207 if not os.path.exists(os.path.join(self.checkout_path, '.git')): | 210 if not os.path.exists(os.path.join(self.checkout_path, '.git')): |
208 raise gclient_utils.Error('\n____ %s%s\n' | 211 raise gclient_utils.Error('\n____ %s%s\n' |
209 '\tPath is not a git repo. No .git dir.\n' | 212 '\tPath is not a git repo. No .git dir.\n' |
210 '\tTo resolve:\n' | 213 '\tTo resolve:\n' |
211 '\t\trm -rf %s\n' | 214 '\t\trm -rf %s\n' |
212 '\tAnd run gclient sync again\n' | 215 '\tAnd run gclient sync again\n' |
213 % (self.relpath, rev_str, self.relpath)) | 216 % (self.relpath, rev_str, self.relpath)) |
214 | 217 |
215 new_base = 'origin' | |
216 if revision: | |
217 new_base = revision | |
218 cur_branch = self._GetCurrentBranch() | 218 cur_branch = self._GetCurrentBranch() |
219 | 219 |
220 # Check if we are in a rebase conflict | 220 # Check if we are in a rebase conflict |
221 if cur_branch is None: | 221 if cur_branch is None: |
222 raise gclient_utils.Error('\n____ %s%s\n' | 222 raise gclient_utils.Error('\n____ %s%s\n' |
223 '\tAlready in a conflict, i.e. (no branch).\n' | 223 '\tAlready in a conflict, i.e. (no branch).\n' |
224 '\tFix the conflict and run gclient again.\n' | 224 '\tFix the conflict and run gclient again.\n' |
225 '\tOr to abort run:\n\t\tgit-rebase --abort\n' | 225 '\tOr to abort run:\n\t\tgit-rebase --abort\n' |
226 '\tSee man git-rebase for details.\n' | 226 '\tSee man git-rebase for details.\n' |
227 % (self.relpath, rev_str)) | 227 % (self.relpath, rev_str)) |
228 | 228 |
229 # TODO(maruel): Do we need to do an automatic retry here? Probably overkill | 229 # Cases: |
230 merge_base = self._Run(['merge-base', 'HEAD', new_base]) | 230 # 1) current branch based on a hash (could be git-svn) |
231 self._Run(['remote', 'update'], redirect_stdout=False) | 231 # - try to rebase onto the new upstream (hash or branch) |
232 files = self._Run(['diff', new_base, '--name-only']).split() | 232 # 2) current branch based on a remote branch with local committed changes, |
| 233 # but the DEPS file switched to point to a hash |
| 234 # - rebase those changes on top of the hash |
| 235 # 3) current branch based on a remote with or without changes, no switch |
| 236 # - see if we can FF, if not, prompt the user for rebase, merge, or stop |
| 237 # 4) current branch based on a remote, switches to a new remote |
| 238 # - exit |
| 239 |
| 240 # GetUpstream returns something like 'refs/remotes/origin/master' for a |
| 241 # tracking branch |
| 242 # or 'master' if not a tracking branch (it's based on a specific rev/hash) |
| 243 # or it returns None if it couldn't find an upstream |
| 244 upstream_branch = self.GetUpstream(self.checkout_path) |
| 245 if not upstream_branch or not upstream_branch.startswith('refs/remotes'): |
| 246 current_type = "hash" |
| 247 logging.debug("Current branch is based off a specific rev and is not " |
| 248 "tracking an upstream.") |
| 249 elif upstream_branch.startswith('refs/remotes'): |
| 250 current_type = "branch" |
| 251 else: |
| 252 raise gclient_utils.Error('Invalid Upstream') |
| 253 |
| 254 # Update the remotes first so we have all the refs |
| 255 remote_output, remote_err = self.Capture(['remote'] + verbose + ['update'], |
| 256 self.checkout_path, |
| 257 print_error=False) |
| 258 if verbose: |
| 259 print remote_output.strip() |
| 260 # git remote update prints to stderr when used with --verbose |
| 261 print remote_err.strip() |
| 262 |
| 263 # This is a big hammer, debatable if it should even be here... |
| 264 if options.force or options.reset: |
| 265 self._Run(['reset', '--hard', 'HEAD'], redirect_stdout=False) |
| 266 |
| 267 if current_type is 'hash': |
| 268 # case 1 |
| 269 if self.IsGitSvn(self.checkout_path) and upstream_branch is not None: |
| 270 # Our git-svn branch (upstream_branch) is our upstream |
| 271 self._AttemptRebase(upstream_branch, files, verbose=options.verbose, |
| 272 newbase=revision, printed_path=printed_path) |
| 273 printed_path = True |
| 274 else: |
| 275 # Can't find a merge-base since we don't know our upstream. That makes |
| 276 # this command VERY likely to produce a rebase failure. For now we |
| 277 # assume origin is our upstream since that's what the old behavior was. |
| 278 self._AttemptRebase('origin', files=files, verbose=options.verbose, |
| 279 printed_path=printed_path) |
| 280 printed_path = True |
| 281 elif rev_type is 'hash': |
| 282 # case 2 |
| 283 self._AttemptRebase(upstream_branch, files, verbose=options.verbose, |
| 284 newbase=revision, printed_path=printed_path) |
| 285 printed_path = True |
| 286 elif revision.replace('heads', 'remotes/origin') != upstream_branch: |
| 287 # case 4 |
| 288 new_base = revision.replace('heads', 'remotes/origin') |
| 289 if not printed_path: |
| 290 print("\n_____ %s%s" % (self.relpath, rev_str)) |
| 291 switch_error = ("Switching upstream branch from %s to %s\n" |
| 292 % (upstream_branch, new_base) + |
| 293 "Please merge or rebase manually:\n" + |
| 294 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) + |
| 295 "OR git checkout -b <some new branch> %s" % new_base) |
| 296 raise gclient_utils.Error(switch_error) |
| 297 else: |
| 298 # case 3 - the default case |
| 299 files = self._Run(['diff', upstream_branch, '--name-only']).split() |
| 300 if verbose: |
| 301 print "Trying fast-forward merge to branch : %s" % upstream_branch |
| 302 try: |
| 303 merge_output, merge_err = self.Capture(['merge', '--ff-only', |
| 304 upstream_branch], |
| 305 self.checkout_path, |
| 306 print_error=False) |
| 307 except gclient_utils.CheckCallError, e: |
| 308 if re.match('fatal: Not possible to fast-forward, aborting.', e.stderr): |
| 309 if not printed_path: |
| 310 print("\n_____ %s%s" % (self.relpath, rev_str)) |
| 311 printed_path = True |
| 312 while True: |
| 313 try: |
| 314 action = str(raw_input("Cannot fast-forward merge, attempt to " |
| 315 "rebase? (y)es / (q)uit / (s)kip : ")) |
| 316 except ValueError: |
| 317 gclient_utils.Error('Invalid Character') |
| 318 continue |
| 319 if re.match(r'yes|y', action, re.I): |
| 320 self._AttemptRebase(upstream_branch, files, |
| 321 verbose=options.verbose, |
| 322 printed_path=printed_path) |
| 323 printed_path = True |
| 324 break |
| 325 elif re.match(r'quit|q', action, re.I): |
| 326 raise gclient_utils.Error("Can't fast-forward, please merge or " |
| 327 "rebase manually.\n" |
| 328 "cd %s && git " % self.checkout_path |
| 329 + "rebase %s" % upstream_branch) |
| 330 elif re.match(r'skip|s', action, re.I): |
| 331 print "Skipping %s" % self.relpath |
| 332 return |
| 333 else: |
| 334 print "Input not recognized" |
| 335 elif re.match("error: Your local changes to '.*' would be " |
| 336 "overwritten by merge. Aborting.\nPlease, commit your " |
| 337 "changes or stash them before you can merge.\n", |
| 338 e.stderr): |
| 339 if not printed_path: |
| 340 print("\n_____ %s%s" % (self.relpath, rev_str)) |
| 341 printed_path = True |
| 342 raise gclient_utils.Error(e.stderr) |
| 343 else: |
| 344 # Some other problem happened with the merge |
| 345 logging.error("Error during fast-forward merge in %s!" % self.relpath) |
| 346 print e.stderr |
| 347 raise |
| 348 else: |
| 349 # Fast-forward merge was successful |
| 350 if not re.match('Already up-to-date.', merge_output) or verbose: |
| 351 if not printed_path: |
| 352 print("\n_____ %s%s" % (self.relpath, rev_str)) |
| 353 printed_path = True |
| 354 print merge_output.strip() |
| 355 if merge_err: |
| 356 print "Merge produced error output:\n%s" % merge_err.strip() |
| 357 if not verbose: |
| 358 # Make the output a little prettier. It's nice to have some |
| 359 # whitespace between projects when syncing. |
| 360 print "" |
| 361 |
233 file_list.extend([os.path.join(self.checkout_path, f) for f in files]) | 362 file_list.extend([os.path.join(self.checkout_path, f) for f in files]) |
234 if options.force or options.reset: | |
235 self._Run(['reset', '--hard', merge_base], redirect_stdout=False) | |
236 try: | |
237 self._Run(['rebase', '-v', '--onto', new_base, merge_base, cur_branch], | |
238 redirect_stdout=False) | |
239 except gclient_utils.Error: | |
240 pass | |
241 | 363 |
242 # If the rebase generated a conflict, abort and ask user to fix | 364 # If the rebase generated a conflict, abort and ask user to fix |
243 if self._GetCurrentBranch() is None: | 365 if self._GetCurrentBranch() is None: |
244 raise gclient_utils.Error('\n____ %s%s\n' | 366 raise gclient_utils.Error('\n____ %s%s\n' |
245 '\nConflict while rebasing this branch.\n' | 367 '\nConflict while rebasing this branch.\n' |
246 'Fix the conflict and run gclient again.\n' | 368 'Fix the conflict and run gclient again.\n' |
247 'See man git-rebase for details.\n' | 369 'See man git-rebase for details.\n' |
248 % (self.relpath, rev_str)) | 370 % (self.relpath, rev_str)) |
249 | 371 |
250 print "Checked out revision %s." % self.revinfo(options, (), None) | 372 if verbose: |
| 373 print "Checked out revision %s" % self.revinfo(options, (), None) |
251 | 374 |
252 def revert(self, options, args, file_list): | 375 def revert(self, options, args, file_list): |
253 """Reverts local modifications. | 376 """Reverts local modifications. |
254 | 377 |
255 All reverted files will be appended to file_list. | 378 All reverted files will be appended to file_list. |
256 """ | 379 """ |
257 __pychecker__ = 'unusednames=args' | 380 __pychecker__ = 'unusednames=args' |
258 path = os.path.join(self._root_dir, self.relpath) | 381 path = os.path.join(self._root_dir, self.relpath) |
259 if not os.path.isdir(path): | 382 if not os.path.isdir(path): |
260 # revert won't work if the directory doesn't exist. It needs to | 383 # revert won't work if the directory doesn't exist. It needs to |
(...skipping 25 matching lines...) Expand all Loading... |
286 self._Run(['diff', '--name-status', merge_base], redirect_stdout=False) | 409 self._Run(['diff', '--name-status', merge_base], redirect_stdout=False) |
287 files = self._Run(['diff', '--name-only', merge_base]).split() | 410 files = self._Run(['diff', '--name-only', merge_base]).split() |
288 file_list.extend([os.path.join(self.checkout_path, f) for f in files]) | 411 file_list.extend([os.path.join(self.checkout_path, f) for f in files]) |
289 | 412 |
290 def FullUrlForRelativeUrl(self, url): | 413 def FullUrlForRelativeUrl(self, url): |
291 # Strip from last '/' | 414 # Strip from last '/' |
292 # Equivalent to unix basename | 415 # Equivalent to unix basename |
293 base_url = self.url | 416 base_url = self.url |
294 return base_url[:base_url.rfind('/')] + url | 417 return base_url[:base_url.rfind('/')] + url |
295 | 418 |
| 419 def _Clone(self, rev_type, revision, url, verbose=False): |
| 420 """Clone a git repository from the given URL. |
| 421 |
| 422 Once we've cloned the repo, we checkout a working branch based off the |
| 423 specified revision.""" |
| 424 if not verbose: |
| 425 # git clone doesn't seem to insert a newline properly before printing |
| 426 # to stdout |
| 427 print "" |
| 428 |
| 429 clone_cmd = ['clone'] |
| 430 if verbose: |
| 431 clone_cmd.append('--verbose') |
| 432 clone_cmd.extend([url, self.checkout_path]) |
| 433 |
| 434 for i in range(3): |
| 435 try: |
| 436 self._Run(clone_cmd, cwd=self._root_dir, redirect_stdout=False) |
| 437 break |
| 438 except gclient_utils.Error, e: |
| 439 # TODO(maruel): Hackish, should be fixed by moving _Run() to |
| 440 # CheckCall(). |
| 441 # Too bad we don't have access to the actual output. |
| 442 # We should check for "transfer closed with NNN bytes remaining to |
| 443 # read". In the meantime, just make sure .git exists. |
| 444 if (e.args[0] == 'git command clone returned 128' and |
| 445 os.path.exists(os.path.join(self.checkout_path, '.git'))): |
| 446 print str(e) |
| 447 print "Retrying..." |
| 448 continue |
| 449 raise e |
| 450 |
| 451 if rev_type is "branch": |
| 452 short_rev = revision.replace('refs/heads/', '') |
| 453 new_branch = revision.replace('heads', 'remotes/origin') |
| 454 elif revision.startswith('refs/tags/'): |
| 455 short_rev = revision.replace('refs/tags/', '') |
| 456 new_branch = revision |
| 457 else: |
| 458 # revision is a specific sha1 hash |
| 459 short_rev = revision |
| 460 new_branch = revision |
| 461 |
| 462 cur_branch = self._GetCurrentBranch() |
| 463 if cur_branch != short_rev: |
| 464 self._Run(['checkout', '-b', short_rev, new_branch], |
| 465 redirect_stdout=False) |
| 466 |
| 467 def _AttemptRebase(self, upstream, files, verbose=False, newbase=None, |
| 468 branch=None, printed_path=False): |
| 469 """Attempt to rebase onto either upstream or, if specified, newbase.""" |
| 470 files.extend(self._Run(['diff', upstream, '--name-only']).split()) |
| 471 revision = upstream |
| 472 if newbase: |
| 473 revision = newbase |
| 474 if not printed_path: |
| 475 print "\n_____ %s : Attempting rebase onto %s..." % (self.relpath, |
| 476 revision) |
| 477 printed_path = True |
| 478 else: |
| 479 print "Attempting rebase onto %s..." % revision |
| 480 |
| 481 # Build the rebase command here using the args |
| 482 # git rebase [options] [--onto <newbase>] <upstream> [<branch>] |
| 483 rebase_cmd = ['rebase'] |
| 484 if verbose: |
| 485 rebase_cmd.append('--verbose') |
| 486 if newbase: |
| 487 rebase_cmd.extend(['--onto', newbase]) |
| 488 rebase_cmd.append(upstream) |
| 489 if branch: |
| 490 rebase_cmd.append(branch) |
| 491 |
| 492 try: |
| 493 rebase_output, rebase_err = self.Capture(rebase_cmd, self.checkout_path, |
| 494 print_error=False) |
| 495 except gclient_utils.CheckCallError, e: |
| 496 if re.match(r'cannot rebase: you have unstaged changes', e.stderr) or \ |
| 497 re.match(r'cannot rebase: your index contains uncommitted changes', |
| 498 e.stderr): |
| 499 while True: |
| 500 rebase_action = str(raw_input("Cannot rebase because of unstaged " |
| 501 "changes.\n'git reset --hard HEAD' ?\n" |
| 502 "WARNING: destroys any uncommitted " |
| 503 "work in your current branch!" |
| 504 " (y)es / (q)uit / (s)how : ")) |
| 505 if re.match(r'yes|y', rebase_action, re.I): |
| 506 self._Run(['reset', '--hard', 'HEAD'], redirect_stdout=False) |
| 507 # Should this be recursive? |
| 508 rebase_output, rebase_err = self.Capture(rebase_cmd, |
| 509 self.checkout_path) |
| 510 break |
| 511 elif re.match(r'quit|q', rebase_action, re.I): |
| 512 raise gclient_utils.Error("Please merge or rebase manually\n" |
| 513 "cd %s && git " % self.checkout_path |
| 514 + "%s" % ' '.join(rebase_cmd)) |
| 515 elif re.match(r'show|s', rebase_action, re.I): |
| 516 print "\n%s" % e.stderr.strip() |
| 517 continue |
| 518 else: |
| 519 gclient_utils.Error("Input not recognized") |
| 520 continue |
| 521 elif re.search(r'^CONFLICT', e.stdout, re.M): |
| 522 raise gclient_utils.Error("Conflict while rebasing this branch.\n" |
| 523 "Fix the conflict and run gclient again.\n" |
| 524 "See 'man git-rebase' for details.\n") |
| 525 else: |
| 526 print e.stdout.strip() |
| 527 print "Rebase produced error output:\n%s" % e.stderr.strip() |
| 528 raise gclient_utils.Error("Unrecognized error, please merge or rebase " |
| 529 "manually.\ncd %s && git " % |
| 530 self.checkout_path |
| 531 + "%s" % ' '.join(rebase_cmd)) |
| 532 |
| 533 print rebase_output.strip() |
| 534 if rebase_err: |
| 535 print "Rebase produced error output:\n%s" % rebase_err.strip() |
| 536 if not verbose: |
| 537 # Make the output a little prettier. It's nice to have some |
| 538 # whitespace between projects when syncing. |
| 539 print "" |
| 540 |
296 def _CheckMinVersion(self, min_version): | 541 def _CheckMinVersion(self, min_version): |
297 def only_int(val): | 542 def only_int(val): |
298 if val.isdigit(): | 543 if val.isdigit(): |
299 return int(val) | 544 return int(val) |
300 else: | 545 else: |
301 return 0 | 546 return 0 |
302 version = self._Run(['--version'], cwd='.').split()[-1] | 547 version = self._Run(['--version'], cwd='.').split()[-1] |
303 version_list = map(only_int, version.split('.')) | 548 version_list = map(only_int, version.split('.')) |
304 min_version_list = map(int, min_version.split('.')) | 549 min_version_list = map(int, min_version.split('.')) |
305 for min_ver in min_version_list: | 550 for min_ver in min_version_list: |
(...skipping 264 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
570 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory " | 815 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory " |
571 "does not exist." | 816 "does not exist." |
572 % (' '.join(command), path)) | 817 % (' '.join(command), path)) |
573 # There's no file list to retrieve. | 818 # There's no file list to retrieve. |
574 else: | 819 else: |
575 self.RunAndGetFileList(options, command, path, file_list) | 820 self.RunAndGetFileList(options, command, path, file_list) |
576 | 821 |
577 def FullUrlForRelativeUrl(self, url): | 822 def FullUrlForRelativeUrl(self, url): |
578 # Find the forth '/' and strip from there. A bit hackish. | 823 # Find the forth '/' and strip from there. A bit hackish. |
579 return '/'.join(self.url.split('/')[:4]) + url | 824 return '/'.join(self.url.split('/')[:4]) + url |
OLD | NEW |