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

Side by Side Diff: gclient_scm.py

Issue 559003: sync @branchname git support (Closed)
Patch Set: final set Created 10 years, 10 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 | tests/gclient_scm_test.py » ('j') | no next file with comments »
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 149 matching lines...) Expand 10 before | Expand all | Expand 10 after
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
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
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
OLDNEW
« no previous file with comments | « no previous file | tests/gclient_scm_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698