summaryrefslogtreecommitdiffstats
path: root/project.py
diff options
context:
space:
mode:
Diffstat (limited to 'project.py')
-rw-r--r--project.py814
1 files changed, 658 insertions, 156 deletions
diff --git a/project.py b/project.py
index b4044943..00ebb17f 100644
--- a/project.py
+++ b/project.py
@@ -12,22 +12,28 @@
12# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
13# limitations under the License. 13# limitations under the License.
14 14
15import traceback
15import errno 16import errno
16import filecmp 17import filecmp
17import os 18import os
19import random
18import re 20import re
19import shutil 21import shutil
20import stat 22import stat
23import subprocess
21import sys 24import sys
22import urllib2 25import time
23 26
24from color import Coloring 27from color import Coloring
25from git_command import GitCommand 28from git_command import GitCommand
26from git_config import GitConfig, IsId 29from git_config import GitConfig, IsId, GetSchemeFromUrl, ID_RE
27from error import GitError, ImportError, UploadError 30from error import DownloadError
31from error import GitError, HookError, ImportError, UploadError
28from error import ManifestInvalidRevisionError 32from error import ManifestInvalidRevisionError
33from progress import Progress
34from trace import IsTrace, Trace
29 35
30from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB 36from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
31 37
32def _lwrite(path, content): 38def _lwrite(path, content):
33 lock = '%s.lock' % path 39 lock = '%s.lock' % path
@@ -54,29 +60,25 @@ def not_rev(r):
54def sq(r): 60def sq(r):
55 return "'" + r.replace("'", "'\''") + "'" 61 return "'" + r.replace("'", "'\''") + "'"
56 62
57hook_list = None 63_project_hook_list = None
58def repo_hooks(): 64def _ProjectHooks():
59 global hook_list 65 """List the hooks present in the 'hooks' directory.
60 if hook_list is None: 66
61 d = os.path.abspath(os.path.dirname(__file__)) 67 These hooks are project hooks and are copied to the '.git/hooks' directory
62 d = os.path.join(d , 'hooks') 68 of all subprojects.
63 hook_list = map(lambda x: os.path.join(d, x), os.listdir(d))
64 return hook_list
65 69
66def relpath(dst, src): 70 This function caches the list of hooks (based on the contents of the
67 src = os.path.dirname(src) 71 'repo/hooks' directory) on the first call.
68 top = os.path.commonprefix([dst, src])
69 if top.endswith('/'):
70 top = top[:-1]
71 else:
72 top = os.path.dirname(top)
73 72
74 tmp = src 73 Returns:
75 rel = '' 74 A list of absolute paths to all of the files in the hooks directory.
76 while top != tmp: 75 """
77 rel += '../' 76 global _project_hook_list
78 tmp = os.path.dirname(tmp) 77 if _project_hook_list is None:
79 return rel + dst[len(top) + 1:] 78 d = os.path.abspath(os.path.dirname(__file__))
79 d = os.path.join(d , 'hooks')
80 _project_hook_list = map(lambda x: os.path.join(d, x), os.listdir(d))
81 return _project_hook_list
80 82
81 83
82class DownloadedChange(object): 84class DownloadedChange(object):
@@ -148,10 +150,11 @@ class ReviewableBranch(object):
148 R_HEADS + self.name, 150 R_HEADS + self.name,
149 '--') 151 '--')
150 152
151 def UploadForReview(self, people, auto_topic=False): 153 def UploadForReview(self, people, auto_topic=False, draft=False):
152 self.project.UploadForReview(self.name, 154 self.project.UploadForReview(self.name,
153 people, 155 people,
154 auto_topic=auto_topic) 156 auto_topic=auto_topic,
157 draft=draft)
155 158
156 def GetPublishedRefs(self): 159 def GetPublishedRefs(self):
157 refs = {} 160 refs = {}
@@ -185,6 +188,11 @@ class DiffColoring(Coloring):
185 Coloring.__init__(self, config, 'diff') 188 Coloring.__init__(self, config, 'diff')
186 self.project = self.printer('header', attr = 'bold') 189 self.project = self.printer('header', attr = 'bold')
187 190
191class _Annotation:
192 def __init__(self, name, value, keep):
193 self.name = name
194 self.value = value
195 self.keep = keep
188 196
189class _CopyFile: 197class _CopyFile:
190 def __init__(self, src, dest, abssrc, absdest): 198 def __init__(self, src, dest, abssrc, absdest):
@@ -223,6 +231,249 @@ class RemoteSpec(object):
223 self.url = url 231 self.url = url
224 self.review = review 232 self.review = review
225 233
234class RepoHook(object):
235 """A RepoHook contains information about a script to run as a hook.
236
237 Hooks are used to run a python script before running an upload (for instance,
238 to run presubmit checks). Eventually, we may have hooks for other actions.
239
240 This shouldn't be confused with files in the 'repo/hooks' directory. Those
241 files are copied into each '.git/hooks' folder for each project. Repo-level
242 hooks are associated instead with repo actions.
243
244 Hooks are always python. When a hook is run, we will load the hook into the
245 interpreter and execute its main() function.
246 """
247 def __init__(self,
248 hook_type,
249 hooks_project,
250 topdir,
251 abort_if_user_denies=False):
252 """RepoHook constructor.
253
254 Params:
255 hook_type: A string representing the type of hook. This is also used
256 to figure out the name of the file containing the hook. For
257 example: 'pre-upload'.
258 hooks_project: The project containing the repo hooks. If you have a
259 manifest, this is manifest.repo_hooks_project. OK if this is None,
260 which will make the hook a no-op.
261 topdir: Repo's top directory (the one containing the .repo directory).
262 Scripts will run with CWD as this directory. If you have a manifest,
263 this is manifest.topdir
264 abort_if_user_denies: If True, we'll throw a HookError() if the user
265 doesn't allow us to run the hook.
266 """
267 self._hook_type = hook_type
268 self._hooks_project = hooks_project
269 self._topdir = topdir
270 self._abort_if_user_denies = abort_if_user_denies
271
272 # Store the full path to the script for convenience.
273 if self._hooks_project:
274 self._script_fullpath = os.path.join(self._hooks_project.worktree,
275 self._hook_type + '.py')
276 else:
277 self._script_fullpath = None
278
279 def _GetHash(self):
280 """Return a hash of the contents of the hooks directory.
281
282 We'll just use git to do this. This hash has the property that if anything
283 changes in the directory we will return a different has.
284
285 SECURITY CONSIDERATION:
286 This hash only represents the contents of files in the hook directory, not
287 any other files imported or called by hooks. Changes to imported files
288 can change the script behavior without affecting the hash.
289
290 Returns:
291 A string representing the hash. This will always be ASCII so that it can
292 be printed to the user easily.
293 """
294 assert self._hooks_project, "Must have hooks to calculate their hash."
295
296 # We will use the work_git object rather than just calling GetRevisionId().
297 # That gives us a hash of the latest checked in version of the files that
298 # the user will actually be executing. Specifically, GetRevisionId()
299 # doesn't appear to change even if a user checks out a different version
300 # of the hooks repo (via git checkout) nor if a user commits their own revs.
301 #
302 # NOTE: Local (non-committed) changes will not be factored into this hash.
303 # I think this is OK, since we're really only worried about warning the user
304 # about upstream changes.
305 return self._hooks_project.work_git.rev_parse('HEAD')
306
307 def _GetMustVerb(self):
308 """Return 'must' if the hook is required; 'should' if not."""
309 if self._abort_if_user_denies:
310 return 'must'
311 else:
312 return 'should'
313
314 def _CheckForHookApproval(self):
315 """Check to see whether this hook has been approved.
316
317 We'll look at the hash of all of the hooks. If this matches the hash that
318 the user last approved, we're done. If it doesn't, we'll ask the user
319 about approval.
320
321 Note that we ask permission for each individual hook even though we use
322 the hash of all hooks when detecting changes. We'd like the user to be
323 able to approve / deny each hook individually. We only use the hash of all
324 hooks because there is no other easy way to detect changes to local imports.
325
326 Returns:
327 True if this hook is approved to run; False otherwise.
328
329 Raises:
330 HookError: Raised if the user doesn't approve and abort_if_user_denies
331 was passed to the consturctor.
332 """
333 hooks_dir = self._hooks_project.worktree
334 hooks_config = self._hooks_project.config
335 git_approval_key = 'repo.hooks.%s.approvedhash' % self._hook_type
336
337 # Get the last hash that the user approved for this hook; may be None.
338 old_hash = hooks_config.GetString(git_approval_key)
339
340 # Get the current hash so we can tell if scripts changed since approval.
341 new_hash = self._GetHash()
342
343 if old_hash is not None:
344 # User previously approved hook and asked not to be prompted again.
345 if new_hash == old_hash:
346 # Approval matched. We're done.
347 return True
348 else:
349 # Give the user a reason why we're prompting, since they last told
350 # us to "never ask again".
351 prompt = 'WARNING: Scripts have changed since %s was allowed.\n\n' % (
352 self._hook_type)
353 else:
354 prompt = ''
355
356 # Prompt the user if we're not on a tty; on a tty we'll assume "no".
357 if sys.stdout.isatty():
358 prompt += ('Repo %s run the script:\n'
359 ' %s\n'
360 '\n'
361 'Do you want to allow this script to run '
362 '(yes/yes-never-ask-again/NO)? ') % (
363 self._GetMustVerb(), self._script_fullpath)
364 response = raw_input(prompt).lower()
365 print
366
367 # User is doing a one-time approval.
368 if response in ('y', 'yes'):
369 return True
370 elif response == 'yes-never-ask-again':
371 hooks_config.SetString(git_approval_key, new_hash)
372 return True
373
374 # For anything else, we'll assume no approval.
375 if self._abort_if_user_denies:
376 raise HookError('You must allow the %s hook or use --no-verify.' %
377 self._hook_type)
378
379 return False
380
381 def _ExecuteHook(self, **kwargs):
382 """Actually execute the given hook.
383
384 This will run the hook's 'main' function in our python interpreter.
385
386 Args:
387 kwargs: Keyword arguments to pass to the hook. These are often specific
388 to the hook type. For instance, pre-upload hooks will contain
389 a project_list.
390 """
391 # Keep sys.path and CWD stashed away so that we can always restore them
392 # upon function exit.
393 orig_path = os.getcwd()
394 orig_syspath = sys.path
395
396 try:
397 # Always run hooks with CWD as topdir.
398 os.chdir(self._topdir)
399
400 # Put the hook dir as the first item of sys.path so hooks can do
401 # relative imports. We want to replace the repo dir as [0] so
402 # hooks can't import repo files.
403 sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:]
404
405 # Exec, storing global context in the context dict. We catch exceptions
406 # and convert to a HookError w/ just the failing traceback.
407 context = {}
408 try:
409 execfile(self._script_fullpath, context)
410 except Exception:
411 raise HookError('%s\nFailed to import %s hook; see traceback above.' % (
412 traceback.format_exc(), self._hook_type))
413
414 # Running the script should have defined a main() function.
415 if 'main' not in context:
416 raise HookError('Missing main() in: "%s"' % self._script_fullpath)
417
418
419 # Add 'hook_should_take_kwargs' to the arguments to be passed to main.
420 # We don't actually want hooks to define their main with this argument--
421 # it's there to remind them that their hook should always take **kwargs.
422 # For instance, a pre-upload hook should be defined like:
423 # def main(project_list, **kwargs):
424 #
425 # This allows us to later expand the API without breaking old hooks.
426 kwargs = kwargs.copy()
427 kwargs['hook_should_take_kwargs'] = True
428
429 # Call the main function in the hook. If the hook should cause the
430 # build to fail, it will raise an Exception. We'll catch that convert
431 # to a HookError w/ just the failing traceback.
432 try:
433 context['main'](**kwargs)
434 except Exception:
435 raise HookError('%s\nFailed to run main() for %s hook; see traceback '
436 'above.' % (
437 traceback.format_exc(), self._hook_type))
438 finally:
439 # Restore sys.path and CWD.
440 sys.path = orig_syspath
441 os.chdir(orig_path)
442
443 def Run(self, user_allows_all_hooks, **kwargs):
444 """Run the hook.
445
446 If the hook doesn't exist (because there is no hooks project or because
447 this particular hook is not enabled), this is a no-op.
448
449 Args:
450 user_allows_all_hooks: If True, we will never prompt about running the
451 hook--we'll just assume it's OK to run it.
452 kwargs: Keyword arguments to pass to the hook. These are often specific
453 to the hook type. For instance, pre-upload hooks will contain
454 a project_list.
455
456 Raises:
457 HookError: If there was a problem finding the hook or the user declined
458 to run a required hook (from _CheckForHookApproval).
459 """
460 # No-op if there is no hooks project or if hook is disabled.
461 if ((not self._hooks_project) or
462 (self._hook_type not in self._hooks_project.enabled_repo_hooks)):
463 return
464
465 # Bail with a nice error if we can't find the hook.
466 if not os.path.isfile(self._script_fullpath):
467 raise HookError('Couldn\'t find repo hook: "%s"' % self._script_fullpath)
468
469 # Make sure the user is OK with running the hook.
470 if (not user_allows_all_hooks) and (not self._CheckForHookApproval()):
471 return
472
473 # Run the hook with the same version of python we're using.
474 self._ExecuteHook(**kwargs)
475
476
226class Project(object): 477class Project(object):
227 def __init__(self, 478 def __init__(self,
228 manifest, 479 manifest,
@@ -232,7 +483,10 @@ class Project(object):
232 worktree, 483 worktree,
233 relpath, 484 relpath,
234 revisionExpr, 485 revisionExpr,
235 revisionId): 486 revisionId,
487 rebase = True,
488 groups = None,
489 sync_c = False):
236 self.manifest = manifest 490 self.manifest = manifest
237 self.name = name 491 self.name = name
238 self.remote = remote 492 self.remote = remote
@@ -251,8 +505,13 @@ class Project(object):
251 else: 505 else:
252 self.revisionId = revisionId 506 self.revisionId = revisionId
253 507
508 self.rebase = rebase
509 self.groups = groups
510 self.sync_c = sync_c
511
254 self.snapshots = {} 512 self.snapshots = {}
255 self.copyfiles = [] 513 self.copyfiles = []
514 self.annotations = []
256 self.config = GitConfig.ForRepository( 515 self.config = GitConfig.ForRepository(
257 gitdir = self.gitdir, 516 gitdir = self.gitdir,
258 defaults = self.manifest.globalConfig) 517 defaults = self.manifest.globalConfig)
@@ -264,6 +523,10 @@ class Project(object):
264 self.bare_git = self._GitGetByExec(self, bare=True) 523 self.bare_git = self._GitGetByExec(self, bare=True)
265 self.bare_ref = GitRefs(gitdir) 524 self.bare_ref = GitRefs(gitdir)
266 525
526 # This will be filled in if a project is later identified to be the
527 # project containing repo hooks.
528 self.enabled_repo_hooks = []
529
267 @property 530 @property
268 def Exists(self): 531 def Exists(self):
269 return os.path.isdir(self.gitdir) 532 return os.path.isdir(self.gitdir)
@@ -367,6 +630,27 @@ class Project(object):
367 630
368 return heads 631 return heads
369 632
633 def MatchesGroups(self, manifest_groups):
634 """Returns true if the manifest groups specified at init should cause
635 this project to be synced.
636 Prefixing a manifest group with "-" inverts the meaning of a group.
637 All projects are implicitly labelled with "default".
638
639 labels are resolved in order. In the example case of
640 project_groups: "default,group1,group2"
641 manifest_groups: "-group1,group2"
642 the project will be matched.
643 """
644 if self.groups is None:
645 return True
646 matched = False
647 for group in manifest_groups:
648 if group.startswith('-') and group[1:] in self.groups:
649 matched = False
650 elif group in self.groups:
651 matched = True
652
653 return matched
370 654
371## Status Display ## 655## Status Display ##
372 656
@@ -391,13 +675,18 @@ class Project(object):
391 675
392 return False 676 return False
393 677
394 def PrintWorkTreeStatus(self): 678 def PrintWorkTreeStatus(self, output_redir=None):
395 """Prints the status of the repository to stdout. 679 """Prints the status of the repository to stdout.
680
681 Args:
682 output: If specified, redirect the output to this object.
396 """ 683 """
397 if not os.path.isdir(self.worktree): 684 if not os.path.isdir(self.worktree):
398 print '' 685 if output_redir == None:
399 print 'project %s/' % self.relpath 686 output_redir = sys.stdout
400 print ' missing (run "repo sync")' 687 print >>output_redir, ''
688 print >>output_redir, 'project %s/' % self.relpath
689 print >>output_redir, ' missing (run "repo sync")'
401 return 690 return
402 691
403 self.work_git.update_index('-q', 692 self.work_git.update_index('-q',
@@ -408,10 +697,12 @@ class Project(object):
408 di = self.work_git.DiffZ('diff-index', '-M', '--cached', HEAD) 697 di = self.work_git.DiffZ('diff-index', '-M', '--cached', HEAD)
409 df = self.work_git.DiffZ('diff-files') 698 df = self.work_git.DiffZ('diff-files')
410 do = self.work_git.LsOthers() 699 do = self.work_git.LsOthers()
411 if not rb and not di and not df and not do: 700 if not rb and not di and not df and not do and not self.CurrentBranch:
412 return 'CLEAN' 701 return 'CLEAN'
413 702
414 out = StatusColoring(self.config) 703 out = StatusColoring(self.config)
704 if not output_redir == None:
705 out.redirect(output_redir)
415 out.project('project %-40s', self.relpath + '/') 706 out.project('project %-40s', self.relpath + '/')
416 707
417 branch = self.CurrentBranch 708 branch = self.CurrentBranch
@@ -461,9 +752,10 @@ class Project(object):
461 else: 752 else:
462 out.write('%s', line) 753 out.write('%s', line)
463 out.nl() 754 out.nl()
755
464 return 'DIRTY' 756 return 'DIRTY'
465 757
466 def PrintWorkTreeDiff(self): 758 def PrintWorkTreeDiff(self, absolute_paths=False):
467 """Prints the status of the repository to stdout. 759 """Prints the status of the repository to stdout.
468 """ 760 """
469 out = DiffColoring(self.config) 761 out = DiffColoring(self.config)
@@ -471,6 +763,9 @@ class Project(object):
471 if out.is_on: 763 if out.is_on:
472 cmd.append('--color') 764 cmd.append('--color')
473 cmd.append(HEAD) 765 cmd.append(HEAD)
766 if absolute_paths:
767 cmd.append('--src-prefix=a/%s/' % self.relpath)
768 cmd.append('--dst-prefix=b/%s/' % self.relpath)
474 cmd.append('--') 769 cmd.append('--')
475 p = GitCommand(self, 770 p = GitCommand(self,
476 cmd, 771 cmd,
@@ -524,7 +819,7 @@ class Project(object):
524 if R_HEADS + n not in heads: 819 if R_HEADS + n not in heads:
525 self.bare_git.DeleteRef(name, id) 820 self.bare_git.DeleteRef(name, id)
526 821
527 def GetUploadableBranches(self): 822 def GetUploadableBranches(self, selected_branch=None):
528 """List any branches which can be uploaded for review. 823 """List any branches which can be uploaded for review.
529 """ 824 """
530 heads = {} 825 heads = {}
@@ -540,6 +835,8 @@ class Project(object):
540 for branch, id in heads.iteritems(): 835 for branch, id in heads.iteritems():
541 if branch in pubed and pubed[branch] == id: 836 if branch in pubed and pubed[branch] == id:
542 continue 837 continue
838 if selected_branch and branch != selected_branch:
839 continue
543 840
544 rb = self.GetUploadableBranch(branch) 841 rb = self.GetUploadableBranch(branch)
545 if rb: 842 if rb:
@@ -559,7 +856,8 @@ class Project(object):
559 856
560 def UploadForReview(self, branch=None, 857 def UploadForReview(self, branch=None,
561 people=([],[]), 858 people=([],[]),
562 auto_topic=False): 859 auto_topic=False,
860 draft=False):
563 """Uploads the named branch for code review. 861 """Uploads the named branch for code review.
564 """ 862 """
565 if branch is None: 863 if branch is None:
@@ -581,31 +879,36 @@ class Project(object):
581 branch.remote.projectname = self.name 879 branch.remote.projectname = self.name
582 branch.remote.Save() 880 branch.remote.Save()
583 881
584 if branch.remote.ReviewProtocol == 'ssh': 882 url = branch.remote.ReviewUrl(self.UserEmail)
585 if dest_branch.startswith(R_HEADS): 883 if url is None:
586 dest_branch = dest_branch[len(R_HEADS):] 884 raise UploadError('review not configured')
885 cmd = ['push']
587 886
887 if url.startswith('ssh://'):
588 rp = ['gerrit receive-pack'] 888 rp = ['gerrit receive-pack']
589 for e in people[0]: 889 for e in people[0]:
590 rp.append('--reviewer=%s' % sq(e)) 890 rp.append('--reviewer=%s' % sq(e))
591 for e in people[1]: 891 for e in people[1]:
592 rp.append('--cc=%s' % sq(e)) 892 rp.append('--cc=%s' % sq(e))
893 cmd.append('--receive-pack=%s' % " ".join(rp))
593 894
594 ref_spec = '%s:refs/for/%s' % (R_HEADS + branch.name, dest_branch) 895 cmd.append(url)
595 if auto_topic:
596 ref_spec = ref_spec + '/' + branch.name
597 896
598 cmd = ['push'] 897 if dest_branch.startswith(R_HEADS):
599 cmd.append('--receive-pack=%s' % " ".join(rp)) 898 dest_branch = dest_branch[len(R_HEADS):]
600 cmd.append(branch.remote.SshReviewUrl(self.UserEmail))
601 cmd.append(ref_spec)
602 899
603 if GitCommand(self, cmd, bare = True).Wait() != 0: 900 upload_type = 'for'
604 raise UploadError('Upload failed') 901 if draft:
902 upload_type = 'drafts'
605 903
606 else: 904 ref_spec = '%s:refs/%s/%s' % (R_HEADS + branch.name, upload_type,
607 raise UploadError('Unsupported protocol %s' \ 905 dest_branch)
608 % branch.remote.review) 906 if auto_topic:
907 ref_spec = ref_spec + '/' + branch.name
908 cmd.append(ref_spec)
909
910 if GitCommand(self, cmd, bare = True).Wait() != 0:
911 raise UploadError('Upload failed')
609 912
610 msg = "posted to %s for %s" % (branch.remote.review, dest_branch) 913 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
611 self.bare_git.UpdateRef(R_PUB + branch.name, 914 self.bare_git.UpdateRef(R_PUB + branch.name,
@@ -615,35 +918,53 @@ class Project(object):
615 918
616## Sync ## 919## Sync ##
617 920
618 def Sync_NetworkHalf(self, quiet=False): 921 def Sync_NetworkHalf(self,
922 quiet=False,
923 is_new=None,
924 current_branch_only=False,
925 clone_bundle=True):
619 """Perform only the network IO portion of the sync process. 926 """Perform only the network IO portion of the sync process.
620 Local working directory/branch state is not affected. 927 Local working directory/branch state is not affected.
621 """ 928 """
622 is_new = not self.Exists 929 if is_new is None:
930 is_new = not self.Exists
623 if is_new: 931 if is_new:
624 if not quiet:
625 print >>sys.stderr
626 print >>sys.stderr, 'Initializing project %s ...' % self.name
627 self._InitGitDir() 932 self._InitGitDir()
628
629 self._InitRemote() 933 self._InitRemote()
630 if not self._RemoteFetch(initial=is_new, quiet=quiet):
631 return False
632 934
633 #Check that the requested ref was found after fetch 935 if is_new:
634 # 936 alt = os.path.join(self.gitdir, 'objects/info/alternates')
635 try: 937 try:
636 self.GetRevisionId() 938 fd = open(alt, 'rb')
637 except ManifestInvalidRevisionError: 939 try:
638 # if the ref is a tag. We can try fetching 940 alt_dir = fd.readline().rstrip()
639 # the tag manually as a last resort 941 finally:
640 # 942 fd.close()
641 rev = self.revisionExpr 943 except IOError:
642 if rev.startswith(R_TAGS): 944 alt_dir = None
643 self._RemoteFetch(None, rev[len(R_TAGS):], quiet=quiet) 945 else:
946 alt_dir = None
947
948 if clone_bundle \
949 and alt_dir is None \
950 and self._ApplyCloneBundle(initial=is_new, quiet=quiet):
951 is_new = False
952
953 if not current_branch_only:
954 if self.sync_c:
955 current_branch_only = True
956 elif not self.manifest._loaded:
957 # Manifest cannot check defaults until it syncs.
958 current_branch_only = False
959 elif self.manifest.default.sync_c:
960 current_branch_only = True
961
962 if not self._RemoteFetch(initial=is_new, quiet=quiet, alt_dir=alt_dir,
963 current_branch_only=current_branch_only):
964 return False
644 965
645 if self.worktree: 966 if self.worktree:
646 self.manifest.SetMRefs(self) 967 self._InitMRef()
647 else: 968 else:
648 self._InitMirrorHead() 969 self._InitMirrorHead()
649 try: 970 try:
@@ -680,11 +1001,11 @@ class Project(object):
680 """Perform only the local IO portion of the sync process. 1001 """Perform only the local IO portion of the sync process.
681 Network access is not required. 1002 Network access is not required.
682 """ 1003 """
683 self._InitWorkTree()
684 all = self.bare_ref.all 1004 all = self.bare_ref.all
685 self.CleanPublishedCache(all) 1005 self.CleanPublishedCache(all)
686
687 revid = self.GetRevisionId(all) 1006 revid = self.GetRevisionId(all)
1007
1008 self._InitWorkTree()
688 head = self.work_git.GetHead() 1009 head = self.work_git.GetHead()
689 if head.startswith(R_HEADS): 1010 if head.startswith(R_HEADS):
690 branch = head[len(R_HEADS):] 1011 branch = head[len(R_HEADS):]
@@ -705,12 +1026,15 @@ class Project(object):
705 1026
706 if head == revid: 1027 if head == revid:
707 # No changes; don't do anything further. 1028 # No changes; don't do anything further.
1029 # Except if the head needs to be detached
708 # 1030 #
709 return 1031 if not syncbuf.detach_head:
1032 return
1033 else:
1034 lost = self._revlist(not_rev(revid), HEAD)
1035 if lost:
1036 syncbuf.info(self, "discarding %d commits", len(lost))
710 1037
711 lost = self._revlist(not_rev(revid), HEAD)
712 if lost:
713 syncbuf.info(self, "discarding %d commits", len(lost))
714 try: 1038 try:
715 self._Checkout(revid, quiet=True) 1039 self._Checkout(revid, quiet=True)
716 except GitError, e: 1040 except GitError, e:
@@ -728,7 +1052,7 @@ class Project(object):
728 1052
729 if not branch.LocalMerge: 1053 if not branch.LocalMerge:
730 # The current branch has no tracking configuration. 1054 # The current branch has no tracking configuration.
731 # Jump off it to a deatched HEAD. 1055 # Jump off it to a detached HEAD.
732 # 1056 #
733 syncbuf.info(self, 1057 syncbuf.info(self,
734 "leaving %s; does not track upstream", 1058 "leaving %s; does not track upstream",
@@ -806,10 +1130,12 @@ class Project(object):
806 len(local_changes) - cnt_mine) 1130 len(local_changes) - cnt_mine)
807 1131
808 branch.remote = self.GetRemote(self.remote.name) 1132 branch.remote = self.GetRemote(self.remote.name)
809 branch.merge = self.revisionExpr 1133 if not ID_RE.match(self.revisionExpr):
1134 # in case of manifest sync the revisionExpr might be a SHA1
1135 branch.merge = self.revisionExpr
810 branch.Save() 1136 branch.Save()
811 1137
812 if cnt_mine > 0: 1138 if cnt_mine > 0 and self.rebase:
813 def _dorebase(): 1139 def _dorebase():
814 self._Rebase(upstream = '%s^1' % last_mine, onto = revid) 1140 self._Rebase(upstream = '%s^1' % last_mine, onto = revid)
815 self._CopyFiles() 1141 self._CopyFiles()
@@ -833,6 +1159,9 @@ class Project(object):
833 abssrc = os.path.join(self.worktree, src) 1159 abssrc = os.path.join(self.worktree, src)
834 self.copyfiles.append(_CopyFile(src, dest, abssrc, absdest)) 1160 self.copyfiles.append(_CopyFile(src, dest, abssrc, absdest))
835 1161
1162 def AddAnnotation(self, name, value, keep):
1163 self.annotations.append(_Annotation(name, value, keep))
1164
836 def DownloadPatchSet(self, change_id, patch_id): 1165 def DownloadPatchSet(self, change_id, patch_id):
837 """Download a single patch set of a single change to FETCH_HEAD. 1166 """Download a single patch set of a single change to FETCH_HEAD.
838 """ 1167 """
@@ -900,6 +1229,13 @@ class Project(object):
900 1229
901 def CheckoutBranch(self, name): 1230 def CheckoutBranch(self, name):
902 """Checkout a local topic branch. 1231 """Checkout a local topic branch.
1232
1233 Args:
1234 name: The name of the branch to checkout.
1235
1236 Returns:
1237 True if the checkout succeeded; False if it didn't; None if the branch
1238 didn't exist.
903 """ 1239 """
904 rev = R_HEADS + name 1240 rev = R_HEADS + name
905 head = self.work_git.GetHead() 1241 head = self.work_git.GetHead()
@@ -914,7 +1250,7 @@ class Project(object):
914 except KeyError: 1250 except KeyError:
915 # Branch does not exist in this project 1251 # Branch does not exist in this project
916 # 1252 #
917 return False 1253 return None
918 1254
919 if head.startswith(R_HEADS): 1255 if head.startswith(R_HEADS):
920 try: 1256 try:
@@ -937,13 +1273,19 @@ class Project(object):
937 1273
938 def AbandonBranch(self, name): 1274 def AbandonBranch(self, name):
939 """Destroy a local topic branch. 1275 """Destroy a local topic branch.
1276
1277 Args:
1278 name: The name of the branch to abandon.
1279
1280 Returns:
1281 True if the abandon succeeded; False if it didn't; None if the branch
1282 didn't exist.
940 """ 1283 """
941 rev = R_HEADS + name 1284 rev = R_HEADS + name
942 all = self.bare_ref.all 1285 all = self.bare_ref.all
943 if rev not in all: 1286 if rev not in all:
944 # Doesn't exist; assume already abandoned. 1287 # Doesn't exist
945 # 1288 return None
946 return True
947 1289
948 head = self.work_git.GetHead() 1290 head = self.work_git.GetHead()
949 if head == rev: 1291 if head == rev:
@@ -1023,31 +1365,43 @@ class Project(object):
1023 1365
1024## Direct Git Commands ## 1366## Direct Git Commands ##
1025 1367
1026 def _RemoteFetch(self, name=None, tag=None, 1368 def _RemoteFetch(self, name=None,
1369 current_branch_only=False,
1027 initial=False, 1370 initial=False,
1028 quiet=False): 1371 quiet=False,
1372 alt_dir=None):
1373
1374 is_sha1 = False
1375 tag_name = None
1376
1377 if current_branch_only:
1378 if ID_RE.match(self.revisionExpr) is not None:
1379 is_sha1 = True
1380 elif self.revisionExpr.startswith(R_TAGS):
1381 # this is a tag and its sha1 value should never change
1382 tag_name = self.revisionExpr[len(R_TAGS):]
1383
1384 if is_sha1 or tag_name is not None:
1385 try:
1386 # if revision (sha or tag) is not present then following function
1387 # throws an error.
1388 self.bare_git.rev_parse('--verify', '%s^0' % self.revisionExpr)
1389 return True
1390 except GitError:
1391 # There is no such persistent revision. We have to fetch it.
1392 pass
1393
1029 if not name: 1394 if not name:
1030 name = self.remote.name 1395 name = self.remote.name
1031 1396
1032 ssh_proxy = False 1397 ssh_proxy = False
1033 if self.GetRemote(name).PreConnectFetch(): 1398 remote = self.GetRemote(name)
1399 if remote.PreConnectFetch():
1034 ssh_proxy = True 1400 ssh_proxy = True
1035 1401
1036 if initial: 1402 if initial:
1037 alt = os.path.join(self.gitdir, 'objects/info/alternates') 1403 if alt_dir and 'objects' == os.path.basename(alt_dir):
1038 try: 1404 ref_dir = os.path.dirname(alt_dir)
1039 fd = open(alt, 'rb')
1040 try:
1041 ref_dir = fd.readline()
1042 if ref_dir and ref_dir.endswith('\n'):
1043 ref_dir = ref_dir[:-1]
1044 finally:
1045 fd.close()
1046 except IOError, e:
1047 ref_dir = None
1048
1049 if ref_dir and 'objects' == os.path.basename(ref_dir):
1050 ref_dir = os.path.dirname(ref_dir)
1051 packed_refs = os.path.join(self.gitdir, 'packed-refs') 1405 packed_refs = os.path.join(self.gitdir, 'packed-refs')
1052 remote = self.GetRemote(name) 1406 remote = self.GetRemote(name)
1053 1407
@@ -1083,35 +1437,130 @@ class Project(object):
1083 old_packed += line 1437 old_packed += line
1084 1438
1085 _lwrite(packed_refs, tmp_packed) 1439 _lwrite(packed_refs, tmp_packed)
1086
1087 else: 1440 else:
1088 ref_dir = None 1441 alt_dir = None
1089 1442
1090 cmd = ['fetch'] 1443 cmd = ['fetch']
1444
1445 # The --depth option only affects the initial fetch; after that we'll do
1446 # full fetches of changes.
1447 depth = self.manifest.manifestProject.config.GetString('repo.depth')
1448 if depth and initial:
1449 cmd.append('--depth=%s' % depth)
1450
1091 if quiet: 1451 if quiet:
1092 cmd.append('--quiet') 1452 cmd.append('--quiet')
1093 if not self.worktree: 1453 if not self.worktree:
1094 cmd.append('--update-head-ok') 1454 cmd.append('--update-head-ok')
1095 cmd.append(name) 1455 cmd.append(name)
1096 if tag is not None:
1097 cmd.append('tag')
1098 cmd.append(tag)
1099 1456
1100 ok = GitCommand(self, 1457 if not current_branch_only or is_sha1:
1101 cmd, 1458 # Fetch whole repo
1102 bare = True, 1459 cmd.append('--tags')
1103 ssh_proxy = ssh_proxy).Wait() == 0 1460 cmd.append((u'+refs/heads/*:') + remote.ToLocal('refs/heads/*'))
1461 elif tag_name is not None:
1462 cmd.append('tag')
1463 cmd.append(tag_name)
1464 else:
1465 branch = self.revisionExpr
1466 if branch.startswith(R_HEADS):
1467 branch = branch[len(R_HEADS):]
1468 cmd.append((u'+refs/heads/%s:' % branch) + remote.ToLocal('refs/heads/%s' % branch))
1469
1470 ok = False
1471 for i in range(2):
1472 if GitCommand(self, cmd, bare=True, ssh_proxy=ssh_proxy).Wait() == 0:
1473 ok = True
1474 break
1475 time.sleep(random.randint(30, 45))
1104 1476
1105 if initial: 1477 if initial:
1106 if ref_dir: 1478 if alt_dir:
1107 if old_packed != '': 1479 if old_packed != '':
1108 _lwrite(packed_refs, old_packed) 1480 _lwrite(packed_refs, old_packed)
1109 else: 1481 else:
1110 os.remove(packed_refs) 1482 os.remove(packed_refs)
1111 self.bare_git.pack_refs('--all', '--prune') 1483 self.bare_git.pack_refs('--all', '--prune')
1484 return ok
1485
1486 def _ApplyCloneBundle(self, initial=False, quiet=False):
1487 if initial and self.manifest.manifestProject.config.GetString('repo.depth'):
1488 return False
1489
1490 remote = self.GetRemote(self.remote.name)
1491 bundle_url = remote.url + '/clone.bundle'
1492 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
1493 if GetSchemeFromUrl(bundle_url) in ('persistent-http', 'persistent-https'):
1494 bundle_url = bundle_url[len('persistent-'):]
1495 if GetSchemeFromUrl(bundle_url) not in ('http', 'https'):
1496 return False
1497
1498 bundle_dst = os.path.join(self.gitdir, 'clone.bundle')
1499 bundle_tmp = os.path.join(self.gitdir, 'clone.bundle.tmp')
1500
1501 exist_dst = os.path.exists(bundle_dst)
1502 exist_tmp = os.path.exists(bundle_tmp)
1503
1504 if not initial and not exist_dst and not exist_tmp:
1505 return False
1112 1506
1507 if not exist_dst:
1508 exist_dst = self._FetchBundle(bundle_url, bundle_tmp, bundle_dst, quiet)
1509 if not exist_dst:
1510 return False
1511
1512 cmd = ['fetch']
1513 if quiet:
1514 cmd.append('--quiet')
1515 if not self.worktree:
1516 cmd.append('--update-head-ok')
1517 cmd.append(bundle_dst)
1518 for f in remote.fetch:
1519 cmd.append(str(f))
1520 cmd.append('refs/tags/*:refs/tags/*')
1521
1522 ok = GitCommand(self, cmd, bare=True).Wait() == 0
1523 if os.path.exists(bundle_dst):
1524 os.remove(bundle_dst)
1525 if os.path.exists(bundle_tmp):
1526 os.remove(bundle_tmp)
1113 return ok 1527 return ok
1114 1528
1529 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet):
1530 if os.path.exists(dstPath):
1531 os.remove(dstPath)
1532
1533 cmd = ['curl', '--output', tmpPath, '--netrc', '--location']
1534 if quiet:
1535 cmd += ['--silent']
1536 if os.path.exists(tmpPath):
1537 size = os.stat(tmpPath).st_size
1538 if size >= 1024:
1539 cmd += ['--continue-at', '%d' % (size,)]
1540 else:
1541 os.remove(tmpPath)
1542 if 'http_proxy' in os.environ and 'darwin' == sys.platform:
1543 cmd += ['--proxy', os.environ['http_proxy']]
1544 cmd += [srcUrl]
1545
1546 if IsTrace():
1547 Trace('%s', ' '.join(cmd))
1548 try:
1549 proc = subprocess.Popen(cmd)
1550 except OSError:
1551 return False
1552
1553 ok = proc.wait() == 0
1554 if os.path.exists(tmpPath):
1555 if ok and os.stat(tmpPath).st_size > 16:
1556 os.rename(tmpPath, dstPath)
1557 return True
1558 else:
1559 os.remove(tmpPath)
1560 return False
1561 else:
1562 return False
1563
1115 def _Checkout(self, rev, quiet=False): 1564 def _Checkout(self, rev, quiet=False):
1116 cmd = ['checkout'] 1565 cmd = ['checkout']
1117 if quiet: 1566 if quiet:
@@ -1122,6 +1571,23 @@ class Project(object):
1122 if self._allrefs: 1571 if self._allrefs:
1123 raise GitError('%s checkout %s ' % (self.name, rev)) 1572 raise GitError('%s checkout %s ' % (self.name, rev))
1124 1573
1574 def _CherryPick(self, rev, quiet=False):
1575 cmd = ['cherry-pick']
1576 cmd.append(rev)
1577 cmd.append('--')
1578 if GitCommand(self, cmd).Wait() != 0:
1579 if self._allrefs:
1580 raise GitError('%s cherry-pick %s ' % (self.name, rev))
1581
1582 def _Revert(self, rev, quiet=False):
1583 cmd = ['revert']
1584 cmd.append('--no-edit')
1585 cmd.append(rev)
1586 cmd.append('--')
1587 if GitCommand(self, cmd).Wait() != 0:
1588 if self._allrefs:
1589 raise GitError('%s revert %s ' % (self.name, rev))
1590
1125 def _ResetHard(self, rev, quiet=True): 1591 def _ResetHard(self, rev, quiet=True):
1126 cmd = ['reset', '--hard'] 1592 cmd = ['reset', '--hard']
1127 if quiet: 1593 if quiet:
@@ -1138,8 +1604,10 @@ class Project(object):
1138 if GitCommand(self, cmd).Wait() != 0: 1604 if GitCommand(self, cmd).Wait() != 0:
1139 raise GitError('%s rebase %s ' % (self.name, upstream)) 1605 raise GitError('%s rebase %s ' % (self.name, upstream))
1140 1606
1141 def _FastForward(self, head): 1607 def _FastForward(self, head, ffonly=False):
1142 cmd = ['merge', head] 1608 cmd = ['merge', head]
1609 if ffonly:
1610 cmd.append("--ff-only")
1143 if GitCommand(self, cmd).Wait() != 0: 1611 if GitCommand(self, cmd).Wait() != 0:
1144 raise GitError('%s merge %s ' % (self.name, head)) 1612 raise GitError('%s merge %s ' % (self.name, head))
1145 1613
@@ -1192,13 +1660,16 @@ class Project(object):
1192 hooks = self._gitdir_path('hooks') 1660 hooks = self._gitdir_path('hooks')
1193 if not os.path.exists(hooks): 1661 if not os.path.exists(hooks):
1194 os.makedirs(hooks) 1662 os.makedirs(hooks)
1195 for stock_hook in repo_hooks(): 1663 for stock_hook in _ProjectHooks():
1196 name = os.path.basename(stock_hook) 1664 name = os.path.basename(stock_hook)
1197 1665
1198 if name in ('commit-msg') and not self.remote.review: 1666 if name in ('commit-msg',) and not self.remote.review \
1667 and not self is self.manifest.manifestProject:
1199 # Don't install a Gerrit Code Review hook if this 1668 # Don't install a Gerrit Code Review hook if this
1200 # project does not appear to use it for reviews. 1669 # project does not appear to use it for reviews.
1201 # 1670 #
1671 # Since the manifest project is one of those, but also
1672 # managed through gerrit, it's excluded
1202 continue 1673 continue
1203 1674
1204 dst = os.path.join(hooks, name) 1675 dst = os.path.join(hooks, name)
@@ -1211,7 +1682,7 @@ class Project(object):
1211 _error("%s: Not replacing %s hook", self.relpath, name) 1682 _error("%s: Not replacing %s hook", self.relpath, name)
1212 continue 1683 continue
1213 try: 1684 try:
1214 os.symlink(relpath(stock_hook, dst), dst) 1685 os.symlink(os.path.relpath(stock_hook, os.path.dirname(dst)), dst)
1215 except OSError, e: 1686 except OSError, e:
1216 if e.errno == errno.EPERM: 1687 if e.errno == errno.EPERM:
1217 raise GitError('filesystem must support symlinks') 1688 raise GitError('filesystem must support symlinks')
@@ -1231,6 +1702,10 @@ class Project(object):
1231 remote.ResetFetch(mirror=True) 1702 remote.ResetFetch(mirror=True)
1232 remote.Save() 1703 remote.Save()
1233 1704
1705 def _InitMRef(self):
1706 if self.manifest.branch:
1707 self._InitAnyMRef(R_M + self.manifest.branch)
1708
1234 def _InitMirrorHead(self): 1709 def _InitMirrorHead(self):
1235 self._InitAnyMRef(HEAD) 1710 self._InitAnyMRef(HEAD)
1236 1711
@@ -1249,40 +1724,33 @@ class Project(object):
1249 msg = 'manifest set to %s' % self.revisionExpr 1724 msg = 'manifest set to %s' % self.revisionExpr
1250 self.bare_git.symbolic_ref('-m', msg, ref, dst) 1725 self.bare_git.symbolic_ref('-m', msg, ref, dst)
1251 1726
1252 def _LinkWorkTree(self, relink=False):
1253 dotgit = os.path.join(self.worktree, '.git')
1254 if not relink:
1255 os.makedirs(dotgit)
1256
1257 for name in ['config',
1258 'description',
1259 'hooks',
1260 'info',
1261 'logs',
1262 'objects',
1263 'packed-refs',
1264 'refs',
1265 'rr-cache',
1266 'svn']:
1267 try:
1268 src = os.path.join(self.gitdir, name)
1269 dst = os.path.join(dotgit, name)
1270 if relink:
1271 os.remove(dst)
1272 if os.path.islink(dst) or not os.path.exists(dst):
1273 os.symlink(relpath(src, dst), dst)
1274 else:
1275 raise GitError('cannot overwrite a local work tree')
1276 except OSError, e:
1277 if e.errno == errno.EPERM:
1278 raise GitError('filesystem must support symlinks')
1279 else:
1280 raise
1281
1282 def _InitWorkTree(self): 1727 def _InitWorkTree(self):
1283 dotgit = os.path.join(self.worktree, '.git') 1728 dotgit = os.path.join(self.worktree, '.git')
1284 if not os.path.exists(dotgit): 1729 if not os.path.exists(dotgit):
1285 self._LinkWorkTree() 1730 os.makedirs(dotgit)
1731
1732 for name in ['config',
1733 'description',
1734 'hooks',
1735 'info',
1736 'logs',
1737 'objects',
1738 'packed-refs',
1739 'refs',
1740 'rr-cache',
1741 'svn']:
1742 try:
1743 src = os.path.join(self.gitdir, name)
1744 dst = os.path.join(dotgit, name)
1745 if os.path.islink(dst) or not os.path.exists(dst):
1746 os.symlink(os.path.relpath(src, os.path.dirname(dst)), dst)
1747 else:
1748 raise GitError('cannot overwrite a local work tree')
1749 except OSError, e:
1750 if e.errno == errno.EPERM:
1751 raise GitError('filesystem must support symlinks')
1752 else:
1753 raise
1286 1754
1287 _lwrite(os.path.join(dotgit, HEAD), '%s\n' % self.GetRevisionId()) 1755 _lwrite(os.path.join(dotgit, HEAD), '%s\n' % self.GetRevisionId())
1288 1756
@@ -1291,6 +1759,11 @@ class Project(object):
1291 cmd.append(HEAD) 1759 cmd.append(HEAD)
1292 if GitCommand(self, cmd).Wait() != 0: 1760 if GitCommand(self, cmd).Wait() != 0:
1293 raise GitError("cannot initialize work tree") 1761 raise GitError("cannot initialize work tree")
1762
1763 rr_cache = os.path.join(self.gitdir, 'rr-cache')
1764 if not os.path.exists(rr_cache):
1765 os.makedirs(rr_cache)
1766
1294 self._CopyFiles() 1767 self._CopyFiles()
1295 1768
1296 def _gitdir_path(self, path): 1769 def _gitdir_path(self, path):
@@ -1449,6 +1922,22 @@ class Project(object):
1449 return r 1922 return r
1450 1923
1451 def __getattr__(self, name): 1924 def __getattr__(self, name):
1925 """Allow arbitrary git commands using pythonic syntax.
1926
1927 This allows you to do things like:
1928 git_obj.rev_parse('HEAD')
1929
1930 Since we don't have a 'rev_parse' method defined, the __getattr__ will
1931 run. We'll replace the '_' with a '-' and try to run a git command.
1932 Any other arguments will be passed to the git command.
1933
1934 Args:
1935 name: The name of the git command to call. Any '_' characters will
1936 be replaced with '-'.
1937
1938 Returns:
1939 A callable object that will try to call git with the named command.
1940 """
1452 name = name.replace('_', '-') 1941 name = name.replace('_', '-')
1453 def runner(*args): 1942 def runner(*args):
1454 cmdv = [name] 1943 cmdv = [name]
@@ -1580,30 +2069,43 @@ class SyncBuffer(object):
1580class MetaProject(Project): 2069class MetaProject(Project):
1581 """A special project housed under .repo. 2070 """A special project housed under .repo.
1582 """ 2071 """
1583 def __init__(self, manifest, name, gitdir, worktree, relpath=None): 2072 def __init__(self, manifest, name, gitdir, worktree):
1584 repodir = manifest.repodir 2073 repodir = manifest.repodir
1585 if relpath is None:
1586 relpath = '.repo/%s' % name
1587 Project.__init__(self, 2074 Project.__init__(self,
1588 manifest = manifest, 2075 manifest = manifest,
1589 name = name, 2076 name = name,
1590 gitdir = gitdir, 2077 gitdir = gitdir,
1591 worktree = worktree, 2078 worktree = worktree,
1592 remote = RemoteSpec('origin'), 2079 remote = RemoteSpec('origin'),
1593 relpath = relpath, 2080 relpath = '.repo/%s' % name,
1594 revisionExpr = 'refs/heads/master', 2081 revisionExpr = 'refs/heads/master',
1595 revisionId = None) 2082 revisionId = None,
2083 groups = None)
1596 2084
1597 def PreSync(self): 2085 def PreSync(self):
1598 if self.Exists: 2086 if self.Exists:
1599 cb = self.CurrentBranch 2087 cb = self.CurrentBranch
1600 if cb: 2088 if cb:
1601 cb = self.GetBranch(cb) 2089 base = self.GetBranch(cb).merge
1602 if cb.merge: 2090 if base:
1603 self.revisionExpr = cb.merge 2091 self.revisionExpr = base
1604 self.revisionId = None 2092 self.revisionId = None
1605 if cb.remote and cb.remote.name: 2093
1606 self.remote.name = cb.remote.name 2094 def MetaBranchSwitch(self, target):
2095 """ Prepare MetaProject for manifest branch switch
2096 """
2097
2098 # detach and delete manifest branch, allowing a new
2099 # branch to take over
2100 syncbuf = SyncBuffer(self.config, detach_head = True)
2101 self.Sync_LocalHalf(syncbuf)
2102 syncbuf.Finish()
2103
2104 return GitCommand(self,
2105 ['update-ref', '-d', 'refs/heads/default'],
2106 capture_stdout = True,
2107 capture_stderr = True).Wait() == 0
2108
1607 2109
1608 @property 2110 @property
1609 def LastFetch(self): 2111 def LastFetch(self):