Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(318)

Side by Side Diff: gclient_scm.py

Issue 1652007: gclient_scm.py: Make working with git more reliable (Closed)
Patch Set: Incorporated feedback and rebased against HEAD Created 10 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | scm.py » ('j') | tests/gclient_scm_test.py » ('J')
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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
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
OLDNEW
« no previous file with comments | « no previous file | scm.py » ('j') | tests/gclient_scm_test.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698