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 181 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
192 rev_type = "branch" | 192 rev_type = "branch" |
193 elif revision.startswith('origin/'): | 193 elif revision.startswith('origin/'): |
194 # For compatability with old naming, translate 'origin' to 'refs/heads' | 194 # For compatability with old naming, translate 'origin' to 'refs/heads' |
195 revision = revision.replace('origin/', 'refs/heads/') | 195 revision = revision.replace('origin/', 'refs/heads/') |
196 rev_type = "branch" | 196 rev_type = "branch" |
197 else: | 197 else: |
198 # hash is also a tag, only make a distinction at checkout | 198 # hash is also a tag, only make a distinction at checkout |
199 rev_type = "hash" | 199 rev_type = "hash" |
200 | 200 |
201 if not os.path.exists(self.checkout_path): | 201 if not os.path.exists(self.checkout_path): |
202 self._Clone(rev_type, revision, url, options.verbose) | 202 self._Clone(revision, url, options.verbose) |
203 files = self._Run(['ls-files']).split() | 203 files = self._Run(['ls-files']).split() |
204 file_list.extend([os.path.join(self.checkout_path, f) for f in files]) | 204 file_list.extend([os.path.join(self.checkout_path, f) for f in files]) |
205 if not verbose: | 205 if not verbose: |
206 # Make the output a little prettier. It's nice to have some whitespace | 206 # Make the output a little prettier. It's nice to have some whitespace |
207 # between projects when cloning. | 207 # between projects when cloning. |
208 print "" | 208 print "" |
209 return | 209 return |
210 | 210 |
211 if not os.path.exists(os.path.join(self.checkout_path, '.git')): | 211 if not os.path.exists(os.path.join(self.checkout_path, '.git')): |
212 raise gclient_utils.Error('\n____ %s%s\n' | 212 raise gclient_utils.Error('\n____ %s%s\n' |
213 '\tPath is not a git repo. No .git dir.\n' | 213 '\tPath is not a git repo. No .git dir.\n' |
214 '\tTo resolve:\n' | 214 '\tTo resolve:\n' |
215 '\t\trm -rf %s\n' | 215 '\t\trm -rf %s\n' |
216 '\tAnd run gclient sync again\n' | 216 '\tAnd run gclient sync again\n' |
217 % (self.relpath, rev_str, self.relpath)) | 217 % (self.relpath, rev_str, self.relpath)) |
218 | 218 |
219 cur_branch = self._GetCurrentBranch() | 219 cur_branch = self._GetCurrentBranch() |
220 | 220 |
221 # Check if we are in a rebase conflict | |
222 if cur_branch is None: | |
223 raise gclient_utils.Error('\n____ %s%s\n' | |
224 '\tAlready in a conflict, i.e. (no branch).\n' | |
225 '\tFix the conflict and run gclient again.\n' | |
226 '\tOr to abort run:\n\t\tgit-rebase --abort\n' | |
227 '\tSee man git-rebase for details.\n' | |
228 % (self.relpath, rev_str)) | |
229 | |
230 # Cases: | 221 # Cases: |
231 # 1) current branch based on a hash (could be git-svn) | 222 # 0) HEAD is detached. Probably from our initial clone. |
232 # - try to rebase onto the new upstream (hash or branch) | 223 # - make sure HEAD is contained by a named ref, then update. |
233 # 2) current branch based on a remote branch with local committed changes, | 224 # Cases 1-4. HEAD is a branch. |
234 # but the DEPS file switched to point to a hash | 225 # 1) current branch is not tracking a remote branch (could be git-svn) |
| 226 # - try to rebase onto the new hash or branch |
| 227 # 2) current branch is tracking a remote branch with local committed |
| 228 # changes, but the DEPS file switched to point to a hash |
235 # - rebase those changes on top of the hash | 229 # - rebase those changes on top of the hash |
236 # 3) current branch based on a remote with or without changes, no switch | 230 # 3) current branch is tracking a remote branch w/or w/out changes, |
| 231 # no switch |
237 # - see if we can FF, if not, prompt the user for rebase, merge, or stop | 232 # - see if we can FF, if not, prompt the user for rebase, merge, or stop |
238 # 4) current branch based on a remote, switches to a new remote | 233 # 4) current branch is tracking a remote branch, switches to a different |
| 234 # remote branch |
239 # - exit | 235 # - exit |
240 | 236 |
241 # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for | 237 # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for |
242 # a tracking branch | 238 # a tracking branch |
243 # or 'master' if not a tracking branch (it's based on a specific rev/hash) | 239 # or 'master' if not a tracking branch (it's based on a specific rev/hash) |
244 # or it returns None if it couldn't find an upstream | 240 # or it returns None if it couldn't find an upstream |
245 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path) | 241 if cur_branch is None: |
246 if not upstream_branch or not upstream_branch.startswith('refs/remotes'): | 242 upstream_branch = None |
247 current_type = "hash" | 243 current_type = "detached" |
248 logging.debug("Current branch is based off a specific rev and is not " | 244 logging.debug("Detached HEAD") |
249 "tracking an upstream.") | |
250 elif upstream_branch.startswith('refs/remotes'): | |
251 current_type = "branch" | |
252 else: | 245 else: |
253 raise gclient_utils.Error('Invalid Upstream') | 246 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path) |
| 247 if not upstream_branch or not upstream_branch.startswith('refs/remotes'): |
| 248 current_type = "hash" |
| 249 logging.debug("Current branch is not tracking an upstream (remote)" |
| 250 " branch.") |
| 251 elif upstream_branch.startswith('refs/remotes'): |
| 252 current_type = "branch" |
| 253 else: |
| 254 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch) |
254 | 255 |
255 # Update the remotes first so we have all the refs. | 256 # Update the remotes first so we have all the refs. |
256 for _ in range(10): | 257 for _ in range(10): |
257 try: | 258 try: |
258 remote_output, remote_err = scm.GIT.Capture( | 259 remote_output, remote_err = scm.GIT.Capture( |
259 ['remote'] + verbose + ['update'], | 260 ['remote'] + verbose + ['update'], |
260 self.checkout_path, | 261 self.checkout_path, |
261 print_error=False) | 262 print_error=False) |
262 break | 263 break |
263 except gclient_utils.CheckCallError: | 264 except gclient_utils.CheckCallError: |
264 # Hackish but at that point, git is known to work so just checking for | 265 # Hackish but at that point, git is known to work so just checking for |
265 # 502 in stderr should be fine. | 266 # 502 in stderr should be fine. |
266 if '502' in e.stderr: | 267 if '502' in e.stderr: |
267 print str(e) | 268 print str(e) |
268 print "Sleeping 15 seconds and retrying..." | 269 print "Sleeping 15 seconds and retrying..." |
269 time.sleep(15) | 270 time.sleep(15) |
270 continue | 271 continue |
271 raise | 272 raise |
272 | 273 |
273 if verbose: | 274 if verbose: |
274 print remote_output.strip() | 275 print remote_output.strip() |
275 # git remote update prints to stderr when used with --verbose | 276 # git remote update prints to stderr when used with --verbose |
276 print remote_err.strip() | 277 print remote_err.strip() |
277 | 278 |
278 # This is a big hammer, debatable if it should even be here... | 279 # This is a big hammer, debatable if it should even be here... |
279 if options.force or options.reset: | 280 if options.force or options.reset: |
280 self._Run(['reset', '--hard', 'HEAD'], redirect_stdout=False) | 281 self._Run(['reset', '--hard', 'HEAD'], redirect_stdout=False) |
281 | 282 |
282 if current_type == 'hash': | 283 if current_type == 'detached': |
| 284 # case 0 |
| 285 self._CheckClean(rev_str) |
| 286 self._CheckDetachedHead(rev_str) |
| 287 self._Run(['checkout', '--quiet', '%s^0' % revision]) |
| 288 if not printed_path: |
| 289 print("\n_____ %s%s" % (self.relpath, rev_str)) |
| 290 elif current_type == 'hash': |
283 # case 1 | 291 # case 1 |
284 if scm.GIT.IsGitSvn(self.checkout_path) and upstream_branch is not None: | 292 if scm.GIT.IsGitSvn(self.checkout_path) and upstream_branch is not None: |
285 # Our git-svn branch (upstream_branch) is our upstream | 293 # Our git-svn branch (upstream_branch) is our upstream |
286 self._AttemptRebase(upstream_branch, files, verbose=options.verbose, | 294 self._AttemptRebase(upstream_branch, files, verbose=options.verbose, |
287 newbase=revision, printed_path=printed_path) | 295 newbase=revision, printed_path=printed_path) |
288 printed_path = True | 296 printed_path = True |
289 else: | 297 else: |
290 # Can't find a merge-base since we don't know our upstream. That makes | 298 # Can't find a merge-base since we don't know our upstream. That makes |
291 # this command VERY likely to produce a rebase failure. For now we | 299 # this command VERY likely to produce a rebase failure. For now we |
292 # assume origin is our upstream since that's what the old behavior was. | 300 # assume origin is our upstream since that's what the old behavior was. |
(...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
373 if merge_err: | 381 if merge_err: |
374 print "Merge produced error output:\n%s" % merge_err.strip() | 382 print "Merge produced error output:\n%s" % merge_err.strip() |
375 if not verbose: | 383 if not verbose: |
376 # Make the output a little prettier. It's nice to have some | 384 # Make the output a little prettier. It's nice to have some |
377 # whitespace between projects when syncing. | 385 # whitespace between projects when syncing. |
378 print "" | 386 print "" |
379 | 387 |
380 file_list.extend([os.path.join(self.checkout_path, f) for f in files]) | 388 file_list.extend([os.path.join(self.checkout_path, f) for f in files]) |
381 | 389 |
382 # If the rebase generated a conflict, abort and ask user to fix | 390 # If the rebase generated a conflict, abort and ask user to fix |
383 if self._GetCurrentBranch() is None: | 391 if self._IsRebasing(): |
384 raise gclient_utils.Error('\n____ %s%s\n' | 392 raise gclient_utils.Error('\n____ %s%s\n' |
385 '\nConflict while rebasing this branch.\n' | 393 '\nConflict while rebasing this branch.\n' |
386 'Fix the conflict and run gclient again.\n' | 394 'Fix the conflict and run gclient again.\n' |
387 'See man git-rebase for details.\n' | 395 'See man git-rebase for details.\n' |
388 % (self.relpath, rev_str)) | 396 % (self.relpath, rev_str)) |
389 | 397 |
390 if verbose: | 398 if verbose: |
391 print "Checked out revision %s" % self.revinfo(options, (), None) | 399 print "Checked out revision %s" % self.revinfo(options, (), None) |
392 | 400 |
393 def revert(self, options, args, file_list): | 401 def revert(self, options, args, file_list): |
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
434 self._Run(['diff', '--name-status', merge_base], redirect_stdout=False) | 442 self._Run(['diff', '--name-status', merge_base], redirect_stdout=False) |
435 files = self._Run(['diff', '--name-only', merge_base]).split() | 443 files = self._Run(['diff', '--name-only', merge_base]).split() |
436 file_list.extend([os.path.join(self.checkout_path, f) for f in files]) | 444 file_list.extend([os.path.join(self.checkout_path, f) for f in files]) |
437 | 445 |
438 def FullUrlForRelativeUrl(self, url): | 446 def FullUrlForRelativeUrl(self, url): |
439 # Strip from last '/' | 447 # Strip from last '/' |
440 # Equivalent to unix basename | 448 # Equivalent to unix basename |
441 base_url = self.url | 449 base_url = self.url |
442 return base_url[:base_url.rfind('/')] + url | 450 return base_url[:base_url.rfind('/')] + url |
443 | 451 |
444 def _Clone(self, rev_type, revision, url, verbose=False): | 452 def _Clone(self, revision, url, verbose=False): |
445 """Clone a git repository from the given URL. | 453 """Clone a git repository from the given URL. |
446 | 454 |
447 Once we've cloned the repo, we checkout a working branch based off the | 455 Once we've cloned the repo, we checkout a working branch if the specified |
448 specified revision.""" | 456 revision is a branch head. If it is a tag or a specific commit, then we |
| 457 leave HEAD detached as it makes future updates simpler -- in this case the |
| 458 user should first create a new branch or switch to an existing branch before |
| 459 making changes in the repo.""" |
449 if not verbose: | 460 if not verbose: |
450 # git clone doesn't seem to insert a newline properly before printing | 461 # git clone doesn't seem to insert a newline properly before printing |
451 # to stdout | 462 # to stdout |
452 print "" | 463 print "" |
453 | 464 |
454 clone_cmd = ['clone'] | 465 clone_cmd = ['clone'] |
| 466 if revision.startswith('refs/heads/'): |
| 467 clone_cmd.extend(['-b', revision.replace('refs/heads/', '')]) |
| 468 detach_head = False |
| 469 else: |
| 470 clone_cmd.append('--no-checkout') |
| 471 detach_head = True |
455 if verbose: | 472 if verbose: |
456 clone_cmd.append('--verbose') | 473 clone_cmd.append('--verbose') |
457 clone_cmd.extend([url, self.checkout_path]) | 474 clone_cmd.extend([url, self.checkout_path]) |
458 | 475 |
459 for _ in range(3): | 476 for _ in range(3): |
460 try: | 477 try: |
461 self._Run(clone_cmd, cwd=self._root_dir, redirect_stdout=False) | 478 self._Run(clone_cmd, cwd=self._root_dir, redirect_stdout=False) |
462 break | 479 break |
463 except gclient_utils.Error, e: | 480 except gclient_utils.Error, e: |
464 # TODO(maruel): Hackish, should be fixed by moving _Run() to | 481 # TODO(maruel): Hackish, should be fixed by moving _Run() to |
465 # CheckCall(). | 482 # CheckCall(). |
466 # Too bad we don't have access to the actual output. | 483 # Too bad we don't have access to the actual output. |
467 # We should check for "transfer closed with NNN bytes remaining to | 484 # We should check for "transfer closed with NNN bytes remaining to |
468 # read". In the meantime, just make sure .git exists. | 485 # read". In the meantime, just make sure .git exists. |
469 if (e.args[0] == 'git command clone returned 128' and | 486 if (e.args[0] == 'git command clone returned 128' and |
470 os.path.exists(os.path.join(self.checkout_path, '.git'))): | 487 os.path.exists(os.path.join(self.checkout_path, '.git'))): |
471 print str(e) | 488 print str(e) |
472 print "Retrying..." | 489 print "Retrying..." |
473 continue | 490 continue |
474 raise e | 491 raise e |
475 | 492 |
476 if rev_type == "branch": | 493 if detach_head: |
477 short_rev = revision.replace('refs/heads/', '') | 494 # Squelch git's very verbose detached HEAD warning and use our own |
478 new_branch = revision.replace('heads', 'remotes/origin') | 495 self._Run(['checkout', '--quiet', '%s^0' % revision]) |
479 elif revision.startswith('refs/tags/'): | 496 print \ |
480 short_rev = revision.replace('refs/tags/', '') | 497 "Checked out %s to a detached HEAD. Before making any commits\n" \ |
481 new_branch = revision | 498 "in this repo, you should use 'git checkout <branch>' to switch to\n" \ |
482 else: | 499 "an existing branch or use 'git checkout origin -b <branch>' to\n" \ |
483 # revision is a specific sha1 hash | 500 "create a new branch for your work." % revision |
484 short_rev = revision | |
485 new_branch = revision | |
486 | |
487 cur_branch = self._GetCurrentBranch() | |
488 if cur_branch != short_rev: | |
489 self._Run(['checkout', '-b', short_rev, new_branch], | |
490 redirect_stdout=False) | |
491 | 501 |
492 def _AttemptRebase(self, upstream, files, verbose=False, newbase=None, | 502 def _AttemptRebase(self, upstream, files, verbose=False, newbase=None, |
493 branch=None, printed_path=False): | 503 branch=None, printed_path=False): |
494 """Attempt to rebase onto either upstream or, if specified, newbase.""" | 504 """Attempt to rebase onto either upstream or, if specified, newbase.""" |
495 files.extend(self._Run(['diff', upstream, '--name-only']).split()) | 505 files.extend(self._Run(['diff', upstream, '--name-only']).split()) |
496 revision = upstream | 506 revision = upstream |
497 if newbase: | 507 if newbase: |
498 revision = newbase | 508 revision = newbase |
499 if not printed_path: | 509 if not printed_path: |
500 print "\n_____ %s : Attempting rebase onto %s..." % (self.relpath, | 510 print "\n_____ %s : Attempting rebase onto %s..." % (self.relpath, |
(...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
563 # Make the output a little prettier. It's nice to have some | 573 # Make the output a little prettier. It's nice to have some |
564 # whitespace between projects when syncing. | 574 # whitespace between projects when syncing. |
565 print "" | 575 print "" |
566 | 576 |
567 def _CheckMinVersion(self, min_version): | 577 def _CheckMinVersion(self, min_version): |
568 (ok, current_version) = scm.GIT.AssertVersion(min_version) | 578 (ok, current_version) = scm.GIT.AssertVersion(min_version) |
569 if not ok: | 579 if not ok: |
570 raise gclient_utils.Error('git version %s < minimum required %s' % | 580 raise gclient_utils.Error('git version %s < minimum required %s' % |
571 (current_version, min_version)) | 581 (current_version, min_version)) |
572 | 582 |
| 583 def _IsRebasing(self): |
| 584 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't |
| 585 # have a plumbing command to determine whether a rebase is in progress, so |
| 586 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash |
| 587 g = os.path.join(self.checkout_path, '.git') |
| 588 return ( |
| 589 os.path.isdir(os.path.join(g, "rebase-merge")) or |
| 590 os.path.isdir(os.path.join(g, "rebase-apply"))) |
| 591 |
| 592 def _CheckClean(self, rev_str): |
| 593 # Make sure the tree is clean; see git-rebase.sh for reference |
| 594 try: |
| 595 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'], |
| 596 self.checkout_path, print_error=False) |
| 597 except gclient_utils.CheckCallError, e: |
| 598 raise gclient_utils.Error('\n____ %s%s\n' |
| 599 '\tYou have unstaged changes.\n' |
| 600 '\tPlease commit, stash, or reset.\n' |
| 601 % (self.relpath, rev_str)) |
| 602 try: |
| 603 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r', |
| 604 '--ignore-submodules', 'HEAD', '--'], self.checkout_path, |
| 605 print_error=False) |
| 606 except gclient_utils.CheckCallError, e: |
| 607 raise gclient_utils.Error('\n____ %s%s\n' |
| 608 '\tYour index contains uncommitted changes\n' |
| 609 '\tPlease commit, stash, or reset.\n' |
| 610 % (self.relpath, rev_str)) |
| 611 |
| 612 def _CheckDetachedHead(self, rev_str): |
| 613 # HEAD is detached. Make sure it is safe to move away from (i.e., it is |
| 614 # reference by a commit). If not, error out -- most likely a rebase is |
| 615 # in progress, try to detect so we can give a better error. |
| 616 try: |
| 617 out, err = scm.GIT.Capture( |
| 618 ['name-rev', '--no-undefined', 'HEAD'], |
| 619 self.checkout_path, |
| 620 print_error=False) |
| 621 except gclient_utils.CheckCallError, e: |
| 622 # Commit is not contained by any rev. See if the user is rebasing: |
| 623 if self._IsRebasing(): |
| 624 # Punt to the user |
| 625 raise gclient_utils.Error('\n____ %s%s\n' |
| 626 '\tAlready in a conflict, i.e. (no branch).\n' |
| 627 '\tFix the conflict and run gclient again.\n' |
| 628 '\tOr to abort run:\n\t\tgit-rebase --abort\n' |
| 629 '\tSee man git-rebase for details.\n' |
| 630 % (self.relpath, rev_str)) |
| 631 # Let's just save off the commit so we can proceed. |
| 632 name = "saved-by-gclient-" + self._Run(["rev-parse", "--short", "HEAD"]) |
| 633 self._Run(["branch", name]) |
| 634 print ("\n_____ found an unreferenced commit and saved it as '%s'" % name) |
| 635 |
573 def _GetCurrentBranch(self): | 636 def _GetCurrentBranch(self): |
574 # Returns name of current branch | 637 # Returns name of current branch or None for detached HEAD |
575 # Returns None if inside a (no branch) | 638 branch = self._Run(['rev-parse', '--abbrev-ref=strict', 'HEAD']) |
576 tokens = self._Run(['branch']).split() | 639 if branch == 'HEAD': |
577 branch = tokens[tokens.index('*') + 1] | |
578 if branch == '(no': | |
579 return None | 640 return None |
580 return branch | 641 return branch |
581 | 642 |
582 def _Run(self, args, cwd=None, redirect_stdout=True): | 643 def _Run(self, args, cwd=None, redirect_stdout=True): |
583 # TODO(maruel): Merge with Capture or better gclient_utils.CheckCall(). | 644 # TODO(maruel): Merge with Capture or better gclient_utils.CheckCall(). |
584 if cwd is None: | 645 if cwd is None: |
585 cwd = self.checkout_path | 646 cwd = self.checkout_path |
586 stdout = None | 647 stdout = None |
587 if redirect_stdout: | 648 if redirect_stdout: |
588 stdout = subprocess.PIPE | 649 stdout = subprocess.PIPE |
(...skipping 270 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
859 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory " | 920 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory " |
860 "does not exist." | 921 "does not exist." |
861 % (' '.join(command), path)) | 922 % (' '.join(command), path)) |
862 # There's no file list to retrieve. | 923 # There's no file list to retrieve. |
863 else: | 924 else: |
864 scm.SVN.RunAndGetFileList(options, command, path, file_list) | 925 scm.SVN.RunAndGetFileList(options, command, path, file_list) |
865 | 926 |
866 def FullUrlForRelativeUrl(self, url): | 927 def FullUrlForRelativeUrl(self, url): |
867 # Find the forth '/' and strip from there. A bit hackish. | 928 # Find the forth '/' and strip from there. A bit hackish. |
868 return '/'.join(self.url.split('/')[:4]) + url | 929 return '/'.join(self.url.split('/')[:4]) + url |
OLD | NEW |