summaryrefslogtreecommitdiffstats
path: root/project.py
diff options
context:
space:
mode:
Diffstat (limited to 'project.py')
-rw-r--r--project.py391
1 files changed, 249 insertions, 142 deletions
diff --git a/project.py b/project.py
index fa4e7cea..0a86a718 100644
--- a/project.py
+++ b/project.py
@@ -30,7 +30,8 @@ import traceback
30 30
31from color import Coloring 31from color import Coloring
32from git_command import GitCommand, git_require 32from git_command import GitCommand, git_require
33from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, ID_RE 33from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \
34 ID_RE
34from error import GitError, HookError, UploadError, DownloadError 35from error import GitError, HookError, UploadError, DownloadError
35from error import ManifestInvalidRevisionError 36from error import ManifestInvalidRevisionError
36from error import NoManifestException 37from error import NoManifestException
@@ -39,11 +40,18 @@ from trace import IsTrace, Trace
39from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M 40from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
40 41
41from pyversion import is_python3 42from pyversion import is_python3
42if not is_python3(): 43if is_python3():
44 import urllib.parse
45else:
46 import imp
47 import urlparse
48 urllib = imp.new_module('urllib')
49 urllib.parse = urlparse
43 # pylint:disable=W0622 50 # pylint:disable=W0622
44 input = raw_input 51 input = raw_input
45 # pylint:enable=W0622 52 # pylint:enable=W0622
46 53
54
47def _lwrite(path, content): 55def _lwrite(path, content):
48 lock = '%s.lock' % path 56 lock = '%s.lock' % path
49 57
@@ -59,21 +67,27 @@ def _lwrite(path, content):
59 os.remove(lock) 67 os.remove(lock)
60 raise 68 raise
61 69
70
62def _error(fmt, *args): 71def _error(fmt, *args):
63 msg = fmt % args 72 msg = fmt % args
64 print('error: %s' % msg, file=sys.stderr) 73 print('error: %s' % msg, file=sys.stderr)
65 74
75
66def _warn(fmt, *args): 76def _warn(fmt, *args):
67 msg = fmt % args 77 msg = fmt % args
68 print('warn: %s' % msg, file=sys.stderr) 78 print('warn: %s' % msg, file=sys.stderr)
69 79
80
70def not_rev(r): 81def not_rev(r):
71 return '^' + r 82 return '^' + r
72 83
84
73def sq(r): 85def sq(r):
74 return "'" + r.replace("'", "'\''") + "'" 86 return "'" + r.replace("'", "'\''") + "'"
75 87
76_project_hook_list = None 88_project_hook_list = None
89
90
77def _ProjectHooks(): 91def _ProjectHooks():
78 """List the hooks present in the 'hooks' directory. 92 """List the hooks present in the 'hooks' directory.
79 93
@@ -107,15 +121,14 @@ class DownloadedChange(object):
107 @property 121 @property
108 def commits(self): 122 def commits(self):
109 if self._commit_cache is None: 123 if self._commit_cache is None:
110 self._commit_cache = self.project.bare_git.rev_list( 124 self._commit_cache = self.project.bare_git.rev_list('--abbrev=8',
111 '--abbrev=8', 125 '--abbrev-commit',
112 '--abbrev-commit', 126 '--pretty=oneline',
113 '--pretty=oneline', 127 '--reverse',
114 '--reverse', 128 '--date-order',
115 '--date-order', 129 not_rev(self.base),
116 not_rev(self.base), 130 self.commit,
117 self.commit, 131 '--')
118 '--')
119 return self._commit_cache 132 return self._commit_cache
120 133
121 134
@@ -134,36 +147,36 @@ class ReviewableBranch(object):
134 @property 147 @property
135 def commits(self): 148 def commits(self):
136 if self._commit_cache is None: 149 if self._commit_cache is None:
137 self._commit_cache = self.project.bare_git.rev_list( 150 self._commit_cache = self.project.bare_git.rev_list('--abbrev=8',
138 '--abbrev=8', 151 '--abbrev-commit',
139 '--abbrev-commit', 152 '--pretty=oneline',
140 '--pretty=oneline', 153 '--reverse',
141 '--reverse', 154 '--date-order',
142 '--date-order', 155 not_rev(self.base),
143 not_rev(self.base), 156 R_HEADS + self.name,
144 R_HEADS + self.name, 157 '--')
145 '--')
146 return self._commit_cache 158 return self._commit_cache
147 159
148 @property 160 @property
149 def unabbrev_commits(self): 161 def unabbrev_commits(self):
150 r = dict() 162 r = dict()
151 for commit in self.project.bare_git.rev_list( 163 for commit in self.project.bare_git.rev_list(not_rev(self.base),
152 not_rev(self.base), 164 R_HEADS + self.name,
153 R_HEADS + self.name, 165 '--'):
154 '--'):
155 r[commit[0:8]] = commit 166 r[commit[0:8]] = commit
156 return r 167 return r
157 168
158 @property 169 @property
159 def date(self): 170 def date(self):
160 return self.project.bare_git.log( 171 return self.project.bare_git.log('--pretty=format:%cd',
161 '--pretty=format:%cd', 172 '-n', '1',
162 '-n', '1', 173 R_HEADS + self.name,
163 R_HEADS + self.name, 174 '--')
164 '--')
165 175
166 def UploadForReview(self, people, auto_topic=False, draft=False, dest_branch=None): 176 def UploadForReview(self, people,
177 auto_topic=False,
178 draft=False,
179 dest_branch=None):
167 self.project.UploadForReview(self.name, 180 self.project.UploadForReview(self.name,
168 people, 181 people,
169 auto_topic=auto_topic, 182 auto_topic=auto_topic,
@@ -173,8 +186,8 @@ class ReviewableBranch(object):
173 def GetPublishedRefs(self): 186 def GetPublishedRefs(self):
174 refs = {} 187 refs = {}
175 output = self.project.bare_git.ls_remote( 188 output = self.project.bare_git.ls_remote(
176 self.branch.remote.SshReviewUrl(self.project.UserEmail), 189 self.branch.remote.SshReviewUrl(self.project.UserEmail),
177 'refs/changes/*') 190 'refs/changes/*')
178 for line in output.split('\n'): 191 for line in output.split('\n'):
179 try: 192 try:
180 (sha, ref) = line.split() 193 (sha, ref) = line.split()
@@ -184,7 +197,9 @@ class ReviewableBranch(object):
184 197
185 return refs 198 return refs
186 199
200
187class StatusColoring(Coloring): 201class StatusColoring(Coloring):
202
188 def __init__(self, config): 203 def __init__(self, config):
189 Coloring.__init__(self, config, 'status') 204 Coloring.__init__(self, config, 'status')
190 self.project = self.printer('header', attr='bold') 205 self.project = self.printer('header', attr='bold')
@@ -198,17 +213,22 @@ class StatusColoring(Coloring):
198 213
199 214
200class DiffColoring(Coloring): 215class DiffColoring(Coloring):
216
201 def __init__(self, config): 217 def __init__(self, config):
202 Coloring.__init__(self, config, 'diff') 218 Coloring.__init__(self, config, 'diff')
203 self.project = self.printer('header', attr='bold') 219 self.project = self.printer('header', attr='bold')
204 220
221
205class _Annotation(object): 222class _Annotation(object):
223
206 def __init__(self, name, value, keep): 224 def __init__(self, name, value, keep):
207 self.name = name 225 self.name = name
208 self.value = value 226 self.value = value
209 self.keep = keep 227 self.keep = keep
210 228
229
211class _CopyFile(object): 230class _CopyFile(object):
231
212 def __init__(self, src, dest, abssrc, absdest): 232 def __init__(self, src, dest, abssrc, absdest):
213 self.src = src 233 self.src = src
214 self.dest = dest 234 self.dest = dest
@@ -236,7 +256,9 @@ class _CopyFile(object):
236 except IOError: 256 except IOError:
237 _error('Cannot copy file %s to %s', src, dest) 257 _error('Cannot copy file %s to %s', src, dest)
238 258
259
239class _LinkFile(object): 260class _LinkFile(object):
261
240 def __init__(self, git_worktree, src, dest, relsrc, absdest): 262 def __init__(self, git_worktree, src, dest, relsrc, absdest):
241 self.git_worktree = git_worktree 263 self.git_worktree = git_worktree
242 self.src = src 264 self.src = src
@@ -275,7 +297,7 @@ class _LinkFile(object):
275 absDestDir = self.abs_dest 297 absDestDir = self.abs_dest
276 if os.path.exists(absDestDir) and not os.path.isdir(absDestDir): 298 if os.path.exists(absDestDir) and not os.path.isdir(absDestDir):
277 _error('Link error: src with wildcard, %s must be a directory', 299 _error('Link error: src with wildcard, %s must be a directory',
278 absDestDir) 300 absDestDir)
279 else: 301 else:
280 absSrcFiles = glob.glob(absSrc) 302 absSrcFiles = glob.glob(absSrc)
281 for absSrcFile in absSrcFiles: 303 for absSrcFile in absSrcFiles:
@@ -292,18 +314,24 @@ class _LinkFile(object):
292 relSrc = os.path.join(relSrcDir, srcFile) 314 relSrc = os.path.join(relSrcDir, srcFile)
293 self.__linkIt(relSrc, absDest) 315 self.__linkIt(relSrc, absDest)
294 316
317
295class RemoteSpec(object): 318class RemoteSpec(object):
319
296 def __init__(self, 320 def __init__(self,
297 name, 321 name,
298 url=None, 322 url=None,
299 review=None, 323 review=None,
300 revision=None): 324 revision=None,
325 orig_name=None):
301 self.name = name 326 self.name = name
302 self.url = url 327 self.url = url
303 self.review = review 328 self.review = review
304 self.revision = revision 329 self.revision = revision
330 self.orig_name = orig_name
331
305 332
306class RepoHook(object): 333class RepoHook(object):
334
307 """A RepoHook contains information about a script to run as a hook. 335 """A RepoHook contains information about a script to run as a hook.
308 336
309 Hooks are used to run a python script before running an upload (for instance, 337 Hooks are used to run a python script before running an upload (for instance,
@@ -316,10 +344,12 @@ class RepoHook(object):
316 Hooks are always python. When a hook is run, we will load the hook into the 344 Hooks are always python. When a hook is run, we will load the hook into the
317 interpreter and execute its main() function. 345 interpreter and execute its main() function.
318 """ 346 """
347
319 def __init__(self, 348 def __init__(self,
320 hook_type, 349 hook_type,
321 hooks_project, 350 hooks_project,
322 topdir, 351 topdir,
352 manifest_url,
323 abort_if_user_denies=False): 353 abort_if_user_denies=False):
324 """RepoHook constructor. 354 """RepoHook constructor.
325 355
@@ -333,11 +363,13 @@ class RepoHook(object):
333 topdir: Repo's top directory (the one containing the .repo directory). 363 topdir: Repo's top directory (the one containing the .repo directory).
334 Scripts will run with CWD as this directory. If you have a manifest, 364 Scripts will run with CWD as this directory. If you have a manifest,
335 this is manifest.topdir 365 this is manifest.topdir
366 manifest_url: The URL to the manifest git repo.
336 abort_if_user_denies: If True, we'll throw a HookError() if the user 367 abort_if_user_denies: If True, we'll throw a HookError() if the user
337 doesn't allow us to run the hook. 368 doesn't allow us to run the hook.
338 """ 369 """
339 self._hook_type = hook_type 370 self._hook_type = hook_type
340 self._hooks_project = hooks_project 371 self._hooks_project = hooks_project
372 self._manifest_url = manifest_url
341 self._topdir = topdir 373 self._topdir = topdir
342 self._abort_if_user_denies = abort_if_user_denies 374 self._abort_if_user_denies = abort_if_user_denies
343 375
@@ -386,9 +418,9 @@ class RepoHook(object):
386 def _CheckForHookApproval(self): 418 def _CheckForHookApproval(self):
387 """Check to see whether this hook has been approved. 419 """Check to see whether this hook has been approved.
388 420
389 We'll look at the hash of all of the hooks. If this matches the hash that 421 We'll accept approval of manifest URLs if they're using secure transports.
390 the user last approved, we're done. If it doesn't, we'll ask the user 422 This way the user can say they trust the manifest hoster. For insecure
391 about approval. 423 hosts, we fall back to checking the hash of the hooks repo.
392 424
393 Note that we ask permission for each individual hook even though we use 425 Note that we ask permission for each individual hook even though we use
394 the hash of all hooks when detecting changes. We'd like the user to be 426 the hash of all hooks when detecting changes. We'd like the user to be
@@ -402,44 +434,58 @@ class RepoHook(object):
402 HookError: Raised if the user doesn't approve and abort_if_user_denies 434 HookError: Raised if the user doesn't approve and abort_if_user_denies
403 was passed to the consturctor. 435 was passed to the consturctor.
404 """ 436 """
405 hooks_config = self._hooks_project.config 437 if self._ManifestUrlHasSecureScheme():
406 git_approval_key = 'repo.hooks.%s.approvedhash' % self._hook_type 438 return self._CheckForHookApprovalManifest()
439 else:
440 return self._CheckForHookApprovalHash()
441
442 def _CheckForHookApprovalHelper(self, subkey, new_val, main_prompt,
443 changed_prompt):
444 """Check for approval for a particular attribute and hook.
407 445
408 # Get the last hash that the user approved for this hook; may be None. 446 Args:
409 old_hash = hooks_config.GetString(git_approval_key) 447 subkey: The git config key under [repo.hooks.<hook_type>] to store the
448 last approved string.
449 new_val: The new value to compare against the last approved one.
450 main_prompt: Message to display to the user to ask for approval.
451 changed_prompt: Message explaining why we're re-asking for approval.
410 452
411 # Get the current hash so we can tell if scripts changed since approval. 453 Returns:
412 new_hash = self._GetHash() 454 True if this hook is approved to run; False otherwise.
413 455
414 if old_hash is not None: 456 Raises:
457 HookError: Raised if the user doesn't approve and abort_if_user_denies
458 was passed to the consturctor.
459 """
460 hooks_config = self._hooks_project.config
461 git_approval_key = 'repo.hooks.%s.%s' % (self._hook_type, subkey)
462
463 # Get the last value that the user approved for this hook; may be None.
464 old_val = hooks_config.GetString(git_approval_key)
465
466 if old_val is not None:
415 # User previously approved hook and asked not to be prompted again. 467 # User previously approved hook and asked not to be prompted again.
416 if new_hash == old_hash: 468 if new_val == old_val:
417 # Approval matched. We're done. 469 # Approval matched. We're done.
418 return True 470 return True
419 else: 471 else:
420 # Give the user a reason why we're prompting, since they last told 472 # Give the user a reason why we're prompting, since they last told
421 # us to "never ask again". 473 # us to "never ask again".
422 prompt = 'WARNING: Scripts have changed since %s was allowed.\n\n' % ( 474 prompt = 'WARNING: %s\n\n' % (changed_prompt,)
423 self._hook_type)
424 else: 475 else:
425 prompt = '' 476 prompt = ''
426 477
427 # Prompt the user if we're not on a tty; on a tty we'll assume "no". 478 # Prompt the user if we're not on a tty; on a tty we'll assume "no".
428 if sys.stdout.isatty(): 479 if sys.stdout.isatty():
429 prompt += ('Repo %s run the script:\n' 480 prompt += main_prompt + ' (yes/always/NO)? '
430 ' %s\n'
431 '\n'
432 'Do you want to allow this script to run '
433 '(yes/yes-never-ask-again/NO)? ') % (
434 self._GetMustVerb(), self._script_fullpath)
435 response = input(prompt).lower() 481 response = input(prompt).lower()
436 print() 482 print()
437 483
438 # User is doing a one-time approval. 484 # User is doing a one-time approval.
439 if response in ('y', 'yes'): 485 if response in ('y', 'yes'):
440 return True 486 return True
441 elif response == 'yes-never-ask-again': 487 elif response == 'always':
442 hooks_config.SetString(git_approval_key, new_hash) 488 hooks_config.SetString(git_approval_key, new_val)
443 return True 489 return True
444 490
445 # For anything else, we'll assume no approval. 491 # For anything else, we'll assume no approval.
@@ -449,6 +495,40 @@ class RepoHook(object):
449 495
450 return False 496 return False
451 497
498 def _ManifestUrlHasSecureScheme(self):
499 """Check if the URI for the manifest is a secure transport."""
500 secure_schemes = ('file', 'https', 'ssh', 'persistent-https', 'sso', 'rpc')
501 parse_results = urllib.parse.urlparse(self._manifest_url)
502 return parse_results.scheme in secure_schemes
503
504 def _CheckForHookApprovalManifest(self):
505 """Check whether the user has approved this manifest host.
506
507 Returns:
508 True if this hook is approved to run; False otherwise.
509 """
510 return self._CheckForHookApprovalHelper(
511 'approvedmanifest',
512 self._manifest_url,
513 'Run hook scripts from %s' % (self._manifest_url,),
514 'Manifest URL has changed since %s was allowed.' % (self._hook_type,))
515
516 def _CheckForHookApprovalHash(self):
517 """Check whether the user has approved the hooks repo.
518
519 Returns:
520 True if this hook is approved to run; False otherwise.
521 """
522 prompt = ('Repo %s run the script:\n'
523 ' %s\n'
524 '\n'
525 'Do you want to allow this script to run')
526 return self._CheckForHookApprovalHelper(
527 'approvedhash',
528 self._GetHash(),
529 prompt % (self._GetMustVerb(), self._script_fullpath),
530 'Scripts have changed since %s was allowed.' % (self._hook_type,))
531
452 def _ExecuteHook(self, **kwargs): 532 def _ExecuteHook(self, **kwargs):
453 """Actually execute the given hook. 533 """Actually execute the given hook.
454 534
@@ -475,19 +555,18 @@ class RepoHook(object):
475 555
476 # Exec, storing global context in the context dict. We catch exceptions 556 # Exec, storing global context in the context dict. We catch exceptions
477 # and convert to a HookError w/ just the failing traceback. 557 # and convert to a HookError w/ just the failing traceback.
478 context = {} 558 context = {'__file__': self._script_fullpath}
479 try: 559 try:
480 exec(compile(open(self._script_fullpath).read(), 560 exec(compile(open(self._script_fullpath).read(),
481 self._script_fullpath, 'exec'), context) 561 self._script_fullpath, 'exec'), context)
482 except Exception: 562 except Exception:
483 raise HookError('%s\nFailed to import %s hook; see traceback above.' % ( 563 raise HookError('%s\nFailed to import %s hook; see traceback above.' %
484 traceback.format_exc(), self._hook_type)) 564 (traceback.format_exc(), self._hook_type))
485 565
486 # Running the script should have defined a main() function. 566 # Running the script should have defined a main() function.
487 if 'main' not in context: 567 if 'main' not in context:
488 raise HookError('Missing main() in: "%s"' % self._script_fullpath) 568 raise HookError('Missing main() in: "%s"' % self._script_fullpath)
489 569
490
491 # Add 'hook_should_take_kwargs' to the arguments to be passed to main. 570 # Add 'hook_should_take_kwargs' to the arguments to be passed to main.
492 # We don't actually want hooks to define their main with this argument-- 571 # We don't actually want hooks to define their main with this argument--
493 # it's there to remind them that their hook should always take **kwargs. 572 # it's there to remind them that their hook should always take **kwargs.
@@ -505,8 +584,8 @@ class RepoHook(object):
505 context['main'](**kwargs) 584 context['main'](**kwargs)
506 except Exception: 585 except Exception:
507 raise HookError('%s\nFailed to run main() for %s hook; see traceback ' 586 raise HookError('%s\nFailed to run main() for %s hook; see traceback '
508 'above.' % ( 587 'above.' % (traceback.format_exc(),
509 traceback.format_exc(), self._hook_type)) 588 self._hook_type))
510 finally: 589 finally:
511 # Restore sys.path and CWD. 590 # Restore sys.path and CWD.
512 sys.path = orig_syspath 591 sys.path = orig_syspath
@@ -530,8 +609,8 @@ class RepoHook(object):
530 to run a required hook (from _CheckForHookApproval). 609 to run a required hook (from _CheckForHookApproval).
531 """ 610 """
532 # No-op if there is no hooks project or if hook is disabled. 611 # No-op if there is no hooks project or if hook is disabled.
533 if ((not self._hooks_project) or 612 if ((not self._hooks_project) or (self._hook_type not in
534 (self._hook_type not in self._hooks_project.enabled_repo_hooks)): 613 self._hooks_project.enabled_repo_hooks)):
535 return 614 return
536 615
537 # Bail with a nice error if we can't find the hook. 616 # Bail with a nice error if we can't find the hook.
@@ -553,6 +632,7 @@ class Project(object):
553 # These objects can only be used by a single working tree. 632 # These objects can only be used by a single working tree.
554 working_tree_files = ['config', 'packed-refs', 'shallow'] 633 working_tree_files = ['config', 'packed-refs', 'shallow']
555 working_tree_dirs = ['logs', 'refs'] 634 working_tree_dirs = ['logs', 'refs']
635
556 def __init__(self, 636 def __init__(self,
557 manifest, 637 manifest,
558 name, 638 name,
@@ -605,15 +685,15 @@ class Project(object):
605 self.gitdir = gitdir.replace('\\', '/') 685 self.gitdir = gitdir.replace('\\', '/')
606 self.objdir = objdir.replace('\\', '/') 686 self.objdir = objdir.replace('\\', '/')
607 if worktree: 687 if worktree:
608 self.worktree = worktree.replace('\\', '/') 688 self.worktree = os.path.normpath(worktree.replace('\\', '/'))
609 else: 689 else:
610 self.worktree = None 690 self.worktree = None
611 self.relpath = relpath 691 self.relpath = relpath
612 self.revisionExpr = revisionExpr 692 self.revisionExpr = revisionExpr
613 693
614 if revisionId is None \ 694 if revisionId is None \
615 and revisionExpr \ 695 and revisionExpr \
616 and IsId(revisionExpr): 696 and IsId(revisionExpr):
617 self.revisionId = revisionExpr 697 self.revisionId = revisionExpr
618 else: 698 else:
619 self.revisionId = revisionId 699 self.revisionId = revisionId
@@ -633,9 +713,8 @@ class Project(object):
633 self.copyfiles = [] 713 self.copyfiles = []
634 self.linkfiles = [] 714 self.linkfiles = []
635 self.annotations = [] 715 self.annotations = []
636 self.config = GitConfig.ForRepository( 716 self.config = GitConfig.ForRepository(gitdir=self.gitdir,
637 gitdir=self.gitdir, 717 defaults=self.manifest.globalConfig)
638 defaults=self.manifest.globalConfig)
639 718
640 if self.worktree: 719 if self.worktree:
641 self.work_git = self._GitGetByExec(self, bare=False, gitdir=gitdir) 720 self.work_git = self._GitGetByExec(self, bare=False, gitdir=gitdir)
@@ -773,7 +852,7 @@ class Project(object):
773 """ 852 """
774 expanded_manifest_groups = manifest_groups or ['default'] 853 expanded_manifest_groups = manifest_groups or ['default']
775 expanded_project_groups = ['all'] + (self.groups or []) 854 expanded_project_groups = ['all'] + (self.groups or [])
776 if not 'notdefault' in expanded_project_groups: 855 if 'notdefault' not in expanded_project_groups:
777 expanded_project_groups += ['default'] 856 expanded_project_groups += ['default']
778 857
779 matched = False 858 matched = False
@@ -785,7 +864,7 @@ class Project(object):
785 864
786 return matched 865 return matched
787 866
788## Status Display ## 867# Status Display ##
789 def UncommitedFiles(self, get_all=True): 868 def UncommitedFiles(self, get_all=True):
790 """Returns a list of strings, uncommitted files in the git tree. 869 """Returns a list of strings, uncommitted files in the git tree.
791 870
@@ -837,7 +916,7 @@ class Project(object):
837 output: If specified, redirect the output to this object. 916 output: If specified, redirect the output to this object.
838 """ 917 """
839 if not os.path.isdir(self.worktree): 918 if not os.path.isdir(self.worktree):
840 if output_redir == None: 919 if output_redir is None:
841 output_redir = sys.stdout 920 output_redir = sys.stdout
842 print(file=output_redir) 921 print(file=output_redir)
843 print('project %s/' % self.relpath, file=output_redir) 922 print('project %s/' % self.relpath, file=output_redir)
@@ -856,7 +935,7 @@ class Project(object):
856 return 'CLEAN' 935 return 'CLEAN'
857 936
858 out = StatusColoring(self.config) 937 out = StatusColoring(self.config)
859 if not output_redir == None: 938 if output_redir is not None:
860 out.redirect(output_redir) 939 out.redirect(output_redir)
861 out.project('project %-40s', self.relpath + '/ ') 940 out.project('project %-40s', self.relpath + '/ ')
862 941
@@ -899,7 +978,7 @@ class Project(object):
899 978
900 if i and i.src_path: 979 if i and i.src_path:
901 line = ' %s%s\t%s => %s (%s%%)' % (i_status, f_status, 980 line = ' %s%s\t%s => %s (%s%%)' % (i_status, f_status,
902 i.src_path, p, i.level) 981 i.src_path, p, i.level)
903 else: 982 else:
904 line = ' %s%s\t%s' % (i_status, f_status, p) 983 line = ' %s%s\t%s' % (i_status, f_status, p)
905 984
@@ -942,7 +1021,7 @@ class Project(object):
942 p.Wait() 1021 p.Wait()
943 1022
944 1023
945## Publish / Upload ## 1024# Publish / Upload ##
946 1025
947 def WasPublished(self, branch, all_refs=None): 1026 def WasPublished(self, branch, all_refs=None):
948 """Was the branch published (uploaded) for code review? 1027 """Was the branch published (uploaded) for code review?
@@ -1085,7 +1164,7 @@ class Project(object):
1085 message=msg) 1164 message=msg)
1086 1165
1087 1166
1088## Sync ## 1167# Sync ##
1089 1168
1090 def _ExtractArchive(self, tarpath, path=None): 1169 def _ExtractArchive(self, tarpath, path=None):
1091 """Extract the given tar on its current location 1170 """Extract the given tar on its current location
@@ -1103,15 +1182,15 @@ class Project(object):
1103 return False 1182 return False
1104 1183
1105 def Sync_NetworkHalf(self, 1184 def Sync_NetworkHalf(self,
1106 quiet=False, 1185 quiet=False,
1107 is_new=None, 1186 is_new=None,
1108 current_branch_only=False, 1187 current_branch_only=False,
1109 force_sync=False, 1188 force_sync=False,
1110 clone_bundle=True, 1189 clone_bundle=True,
1111 no_tags=False, 1190 no_tags=False,
1112 archive=False, 1191 archive=False,
1113 optimized_fetch=False, 1192 optimized_fetch=False,
1114 prune=False): 1193 prune=False):
1115 """Perform only the network IO portion of the sync process. 1194 """Perform only the network IO portion of the sync process.
1116 Local working directory/branch state is not affected. 1195 Local working directory/branch state is not affected.
1117 """ 1196 """
@@ -1164,8 +1243,8 @@ class Project(object):
1164 alt_dir = None 1243 alt_dir = None
1165 1244
1166 if clone_bundle \ 1245 if clone_bundle \
1167 and alt_dir is None \ 1246 and alt_dir is None \
1168 and self._ApplyCloneBundle(initial=is_new, quiet=quiet): 1247 and self._ApplyCloneBundle(initial=is_new, quiet=quiet):
1169 is_new = False 1248 is_new = False
1170 1249
1171 if not current_branch_only: 1250 if not current_branch_only:
@@ -1177,12 +1256,13 @@ class Project(object):
1177 elif self.manifest.default.sync_c: 1256 elif self.manifest.default.sync_c:
1178 current_branch_only = True 1257 current_branch_only = True
1179 1258
1180 need_to_fetch = not (optimized_fetch and \ 1259 need_to_fetch = not (optimized_fetch and
1181 (ID_RE.match(self.revisionExpr) and self._CheckForSha1())) 1260 (ID_RE.match(self.revisionExpr) and
1182 if (need_to_fetch 1261 self._CheckForSha1()))
1183 and not self._RemoteFetch(initial=is_new, quiet=quiet, alt_dir=alt_dir, 1262 if (need_to_fetch and
1184 current_branch_only=current_branch_only, 1263 not self._RemoteFetch(initial=is_new, quiet=quiet, alt_dir=alt_dir,
1185 no_tags=no_tags, prune=prune)): 1264 current_branch_only=current_branch_only,
1265 no_tags=no_tags, prune=prune)):
1186 return False 1266 return False
1187 1267
1188 if self.worktree: 1268 if self.worktree:
@@ -1219,9 +1299,8 @@ class Project(object):
1219 try: 1299 try:
1220 return self.bare_git.rev_list(self.revisionExpr, '-1')[0] 1300 return self.bare_git.rev_list(self.revisionExpr, '-1')[0]
1221 except GitError: 1301 except GitError:
1222 raise ManifestInvalidRevisionError( 1302 raise ManifestInvalidRevisionError('revision %s in %s not found' %
1223 'revision %s in %s not found' % (self.revisionExpr, 1303 (self.revisionExpr, self.name))
1224 self.name))
1225 1304
1226 def GetRevisionId(self, all_refs=None): 1305 def GetRevisionId(self, all_refs=None):
1227 if self.revisionId: 1306 if self.revisionId:
@@ -1236,9 +1315,8 @@ class Project(object):
1236 try: 1315 try:
1237 return self.bare_git.rev_parse('--verify', '%s^0' % rev) 1316 return self.bare_git.rev_parse('--verify', '%s^0' % rev)
1238 except GitError: 1317 except GitError:
1239 raise ManifestInvalidRevisionError( 1318 raise ManifestInvalidRevisionError('revision %s in %s not found' %
1240 'revision %s in %s not found' % (self.revisionExpr, 1319 (self.revisionExpr, self.name))
1241 self.name))
1242 1320
1243 def Sync_LocalHalf(self, syncbuf, force_sync=False): 1321 def Sync_LocalHalf(self, syncbuf, force_sync=False):
1244 """Perform only the local IO portion of the sync process. 1322 """Perform only the local IO portion of the sync process.
@@ -1327,8 +1405,8 @@ class Project(object):
1327 # to rewrite the published commits so we punt. 1405 # to rewrite the published commits so we punt.
1328 # 1406 #
1329 syncbuf.fail(self, 1407 syncbuf.fail(self,
1330 "branch %s is published (but not merged) and is now %d commits behind" 1408 "branch %s is published (but not merged) and is now "
1331 % (branch.name, len(upstream_gain))) 1409 "%d commits behind" % (branch.name, len(upstream_gain)))
1332 return 1410 return
1333 elif pub == head: 1411 elif pub == head:
1334 # All published commits are merged, and thus we are a 1412 # All published commits are merged, and thus we are a
@@ -1422,7 +1500,7 @@ class Project(object):
1422 remote = self.GetRemote(self.remote.name) 1500 remote = self.GetRemote(self.remote.name)
1423 1501
1424 cmd = ['fetch', remote.name] 1502 cmd = ['fetch', remote.name]
1425 cmd.append('refs/changes/%2.2d/%d/%d' \ 1503 cmd.append('refs/changes/%2.2d/%d/%d'
1426 % (change_id % 100, change_id, patch_id)) 1504 % (change_id % 100, change_id, patch_id))
1427 if GitCommand(self, cmd, bare=True).Wait() != 0: 1505 if GitCommand(self, cmd, bare=True).Wait() != 0:
1428 return None 1506 return None
@@ -1433,7 +1511,7 @@ class Project(object):
1433 self.bare_git.rev_parse('FETCH_HEAD')) 1511 self.bare_git.rev_parse('FETCH_HEAD'))
1434 1512
1435 1513
1436## Branch Management ## 1514# Branch Management ##
1437 1515
1438 def StartBranch(self, name, branch_merge=''): 1516 def StartBranch(self, name, branch_merge=''):
1439 """Create a new branch off the manifest's revision. 1517 """Create a new branch off the manifest's revision.
@@ -1620,10 +1698,11 @@ class Project(object):
1620 return kept 1698 return kept
1621 1699
1622 1700
1623## Submodule Management ## 1701# Submodule Management ##
1624 1702
1625 def GetRegisteredSubprojects(self): 1703 def GetRegisteredSubprojects(self):
1626 result = [] 1704 result = []
1705
1627 def rec(subprojects): 1706 def rec(subprojects):
1628 if not subprojects: 1707 if not subprojects:
1629 return 1708 return
@@ -1658,6 +1737,7 @@ class Project(object):
1658 1737
1659 re_path = re.compile(r'^submodule\.([^.]+)\.path=(.*)$') 1738 re_path = re.compile(r'^submodule\.([^.]+)\.path=(.*)$')
1660 re_url = re.compile(r'^submodule\.([^.]+)\.url=(.*)$') 1739 re_url = re.compile(r'^submodule\.([^.]+)\.url=(.*)$')
1740
1661 def parse_gitmodules(gitdir, rev): 1741 def parse_gitmodules(gitdir, rev):
1662 cmd = ['cat-file', 'blob', '%s:.gitmodules' % rev] 1742 cmd = ['cat-file', 'blob', '%s:.gitmodules' % rev]
1663 try: 1743 try:
@@ -1767,7 +1847,7 @@ class Project(object):
1767 return result 1847 return result
1768 1848
1769 1849
1770## Direct Git Commands ## 1850# Direct Git Commands ##
1771 def _CheckForSha1(self): 1851 def _CheckForSha1(self):
1772 try: 1852 try:
1773 # if revision (sha or tag) is not present then following function 1853 # if revision (sha or tag) is not present then following function
@@ -1791,7 +1871,6 @@ class Project(object):
1791 if command.Wait() != 0: 1871 if command.Wait() != 0:
1792 raise GitError('git archive %s: %s' % (self.name, command.stderr)) 1872 raise GitError('git archive %s: %s' % (self.name, command.stderr))
1793 1873
1794
1795 def _RemoteFetch(self, name=None, 1874 def _RemoteFetch(self, name=None,
1796 current_branch_only=False, 1875 current_branch_only=False,
1797 initial=False, 1876 initial=False,
@@ -1838,7 +1917,10 @@ class Project(object):
1838 # will fail. 1917 # will fail.
1839 # * otherwise, fetch all branches to make sure we end up with the 1918 # * otherwise, fetch all branches to make sure we end up with the
1840 # specific commit. 1919 # specific commit.
1841 current_branch_only = self.upstream and not ID_RE.match(self.upstream) 1920 if self.upstream:
1921 current_branch_only = not ID_RE.match(self.upstream)
1922 else:
1923 current_branch_only = False
1842 1924
1843 if not name: 1925 if not name:
1844 name = self.remote.name 1926 name = self.remote.name
@@ -1955,9 +2037,9 @@ class Project(object):
1955 break 2037 break
1956 continue 2038 continue
1957 elif current_branch_only and is_sha1 and ret == 128: 2039 elif current_branch_only and is_sha1 and ret == 128:
1958 # Exit code 128 means "couldn't find the ref you asked for"; if we're in sha1 2040 # Exit code 128 means "couldn't find the ref you asked for"; if we're
1959 # mode, we just tried sync'ing from the upstream field; it doesn't exist, thus 2041 # in sha1 mode, we just tried sync'ing from the upstream field; it
1960 # abort the optimization attempt and do a full sync. 2042 # doesn't exist, thus abort the optimization attempt and do a full sync.
1961 break 2043 break
1962 elif ret < 0: 2044 elif ret < 0:
1963 # Git died with a signal, exit immediately 2045 # Git died with a signal, exit immediately
@@ -1984,20 +2066,24 @@ class Project(object):
1984 initial=False, quiet=quiet, alt_dir=alt_dir) 2066 initial=False, quiet=quiet, alt_dir=alt_dir)
1985 if self.clone_depth: 2067 if self.clone_depth:
1986 self.clone_depth = None 2068 self.clone_depth = None
1987 return self._RemoteFetch(name=name, current_branch_only=current_branch_only, 2069 return self._RemoteFetch(name=name,
2070 current_branch_only=current_branch_only,
1988 initial=False, quiet=quiet, alt_dir=alt_dir) 2071 initial=False, quiet=quiet, alt_dir=alt_dir)
1989 2072
1990 return ok 2073 return ok
1991 2074
1992 def _ApplyCloneBundle(self, initial=False, quiet=False): 2075 def _ApplyCloneBundle(self, initial=False, quiet=False):
1993 if initial and (self.manifest.manifestProject.config.GetString('repo.depth') or self.clone_depth): 2076 if initial and \
2077 (self.manifest.manifestProject.config.GetString('repo.depth') or
2078 self.clone_depth):
1994 return False 2079 return False
1995 2080
1996 remote = self.GetRemote(self.remote.name) 2081 remote = self.GetRemote(self.remote.name)
1997 bundle_url = remote.url + '/clone.bundle' 2082 bundle_url = remote.url + '/clone.bundle'
1998 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url) 2083 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
1999 if GetSchemeFromUrl(bundle_url) not in ( 2084 if GetSchemeFromUrl(bundle_url) not in ('http', 'https',
2000 'http', 'https', 'persistent-http', 'persistent-https'): 2085 'persistent-http',
2086 'persistent-https'):
2001 return False 2087 return False
2002 2088
2003 bundle_dst = os.path.join(self.gitdir, 'clone.bundle') 2089 bundle_dst = os.path.join(self.gitdir, 'clone.bundle')
@@ -2046,7 +2132,7 @@ class Project(object):
2046 os.remove(tmpPath) 2132 os.remove(tmpPath)
2047 if 'http_proxy' in os.environ and 'darwin' == sys.platform: 2133 if 'http_proxy' in os.environ and 'darwin' == sys.platform:
2048 cmd += ['--proxy', os.environ['http_proxy']] 2134 cmd += ['--proxy', os.environ['http_proxy']]
2049 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy): 2135 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, _proxy):
2050 if cookiefile: 2136 if cookiefile:
2051 cmd += ['--cookie', cookiefile, '--cookie-jar', cookiefile] 2137 cmd += ['--cookie', cookiefile, '--cookie-jar', cookiefile]
2052 if srcUrl.startswith('persistent-'): 2138 if srcUrl.startswith('persistent-'):
@@ -2165,11 +2251,12 @@ class Project(object):
2165 self._CheckDirReference(self.objdir, self.gitdir, share_refs=False) 2251 self._CheckDirReference(self.objdir, self.gitdir, share_refs=False)
2166 except GitError as e: 2252 except GitError as e:
2167 if force_sync: 2253 if force_sync:
2168 print("Retrying clone after deleting %s" % self.gitdir, file=sys.stderr) 2254 print("Retrying clone after deleting %s" %
2255 self.gitdir, file=sys.stderr)
2169 try: 2256 try:
2170 shutil.rmtree(os.path.realpath(self.gitdir)) 2257 shutil.rmtree(os.path.realpath(self.gitdir))
2171 if self.worktree and os.path.exists( 2258 if self.worktree and os.path.exists(os.path.realpath
2172 os.path.realpath(self.worktree)): 2259 (self.worktree)):
2173 shutil.rmtree(os.path.realpath(self.worktree)) 2260 shutil.rmtree(os.path.realpath(self.worktree))
2174 return self._InitGitDir(mirror_git=mirror_git, force_sync=False) 2261 return self._InitGitDir(mirror_git=mirror_git, force_sync=False)
2175 except: 2262 except:
@@ -2205,6 +2292,7 @@ class Project(object):
2205 for key in ['user.name', 'user.email']: 2292 for key in ['user.name', 'user.email']:
2206 if m.Has(key, include_defaults=False): 2293 if m.Has(key, include_defaults=False):
2207 self.config.SetString(key, m.GetString(key)) 2294 self.config.SetString(key, m.GetString(key))
2295 self.config.SetString('filter.lfs.smudge', 'git-lfs smudge --skip -- %f')
2208 if self.manifest.IsMirror: 2296 if self.manifest.IsMirror:
2209 self.config.SetString('core.bare', 'true') 2297 self.config.SetString('core.bare', 'true')
2210 else: 2298 else:
@@ -2228,7 +2316,7 @@ class Project(object):
2228 name = os.path.basename(stock_hook) 2316 name = os.path.basename(stock_hook)
2229 2317
2230 if name in ('commit-msg',) and not self.remote.review \ 2318 if name in ('commit-msg',) and not self.remote.review \
2231 and not self is self.manifest.manifestProject: 2319 and self is not self.manifest.manifestProject:
2232 # Don't install a Gerrit Code Review hook if this 2320 # Don't install a Gerrit Code Review hook if this
2233 # project does not appear to use it for reviews. 2321 # project does not appear to use it for reviews.
2234 # 2322 #
@@ -2243,7 +2331,8 @@ class Project(object):
2243 if filecmp.cmp(stock_hook, dst, shallow=False): 2331 if filecmp.cmp(stock_hook, dst, shallow=False):
2244 os.remove(dst) 2332 os.remove(dst)
2245 else: 2333 else:
2246 _warn("%s: Not replacing locally modified %s hook", self.relpath, name) 2334 _warn("%s: Not replacing locally modified %s hook",
2335 self.relpath, name)
2247 continue 2336 continue
2248 try: 2337 try:
2249 os.symlink(os.path.relpath(stock_hook, os.path.dirname(dst)), dst) 2338 os.symlink(os.path.relpath(stock_hook, os.path.dirname(dst)), dst)
@@ -2289,8 +2378,8 @@ class Project(object):
2289 self.bare_git.symbolic_ref('-m', msg, ref, dst) 2378 self.bare_git.symbolic_ref('-m', msg, ref, dst)
2290 2379
2291 def _CheckDirReference(self, srcdir, destdir, share_refs): 2380 def _CheckDirReference(self, srcdir, destdir, share_refs):
2292 symlink_files = self.shareable_files 2381 symlink_files = self.shareable_files[:]
2293 symlink_dirs = self.shareable_dirs 2382 symlink_dirs = self.shareable_dirs[:]
2294 if share_refs: 2383 if share_refs:
2295 symlink_files += self.working_tree_files 2384 symlink_files += self.working_tree_files
2296 symlink_dirs += self.working_tree_dirs 2385 symlink_dirs += self.working_tree_dirs
@@ -2318,8 +2407,8 @@ class Project(object):
2318 copy_all: If true, copy all remaining files from |gitdir| -> |dotgit|. 2407 copy_all: If true, copy all remaining files from |gitdir| -> |dotgit|.
2319 This saves you the effort of initializing |dotgit| yourself. 2408 This saves you the effort of initializing |dotgit| yourself.
2320 """ 2409 """
2321 symlink_files = self.shareable_files 2410 symlink_files = self.shareable_files[:]
2322 symlink_dirs = self.shareable_dirs 2411 symlink_dirs = self.shareable_dirs[:]
2323 if share_refs: 2412 if share_refs:
2324 symlink_files += self.working_tree_files 2413 symlink_files += self.working_tree_files
2325 symlink_dirs += self.working_tree_dirs 2414 symlink_dirs += self.working_tree_dirs
@@ -2411,7 +2500,7 @@ class Project(object):
2411 def _allrefs(self): 2500 def _allrefs(self):
2412 return self.bare_ref.all 2501 return self.bare_ref.all
2413 2502
2414 def _getLogs(self, rev1, rev2, oneline=False, color=True): 2503 def _getLogs(self, rev1, rev2, oneline=False, color=True, pretty_format=None):
2415 """Get logs between two revisions of this project.""" 2504 """Get logs between two revisions of this project."""
2416 comp = '..' 2505 comp = '..'
2417 if rev1: 2506 if rev1:
@@ -2422,6 +2511,8 @@ class Project(object):
2422 out = DiffColoring(self.config) 2511 out = DiffColoring(self.config)
2423 if out.is_on and color: 2512 if out.is_on and color:
2424 cmd.append('--color') 2513 cmd.append('--color')
2514 if pretty_format is not None:
2515 cmd.append('--pretty=format:%s' % pretty_format)
2425 if oneline: 2516 if oneline:
2426 cmd.append('--oneline') 2517 cmd.append('--oneline')
2427 2518
@@ -2438,17 +2529,21 @@ class Project(object):
2438 raise 2529 raise
2439 return None 2530 return None
2440 2531
2441 def getAddedAndRemovedLogs(self, toProject, oneline=False, color=True): 2532 def getAddedAndRemovedLogs(self, toProject, oneline=False, color=True,
2533 pretty_format=None):
2442 """Get the list of logs from this revision to given revisionId""" 2534 """Get the list of logs from this revision to given revisionId"""
2443 logs = {} 2535 logs = {}
2444 selfId = self.GetRevisionId(self._allrefs) 2536 selfId = self.GetRevisionId(self._allrefs)
2445 toId = toProject.GetRevisionId(toProject._allrefs) 2537 toId = toProject.GetRevisionId(toProject._allrefs)
2446 2538
2447 logs['added'] = self._getLogs(selfId, toId, oneline=oneline, color=color) 2539 logs['added'] = self._getLogs(selfId, toId, oneline=oneline, color=color,
2448 logs['removed'] = self._getLogs(toId, selfId, oneline=oneline, color=color) 2540 pretty_format=pretty_format)
2541 logs['removed'] = self._getLogs(toId, selfId, oneline=oneline, color=color,
2542 pretty_format=pretty_format)
2449 return logs 2543 return logs
2450 2544
2451 class _GitGetByExec(object): 2545 class _GitGetByExec(object):
2546
2452 def __init__(self, project, bare, gitdir): 2547 def __init__(self, project, bare, gitdir):
2453 self._project = project 2548 self._project = project
2454 self._bare = bare 2549 self._bare = bare
@@ -2467,8 +2562,8 @@ class Project(object):
2467 if p.Wait() == 0: 2562 if p.Wait() == 0:
2468 out = p.stdout 2563 out = p.stdout
2469 if out: 2564 if out:
2565 # Backslash is not anomalous
2470 return out[:-1].split('\0') # pylint: disable=W1401 2566 return out[:-1].split('\0') # pylint: disable=W1401
2471 # Backslash is not anomalous
2472 return [] 2567 return []
2473 2568
2474 def DiffZ(self, name, *args): 2569 def DiffZ(self, name, *args):
@@ -2494,6 +2589,7 @@ class Project(object):
2494 break 2589 break
2495 2590
2496 class _Info(object): 2591 class _Info(object):
2592
2497 def __init__(self, path, omode, nmode, oid, nid, state): 2593 def __init__(self, path, omode, nmode, oid, nid, state):
2498 self.path = path 2594 self.path = path
2499 self.src_path = None 2595 self.src_path = None
@@ -2596,10 +2692,8 @@ class Project(object):
2596 line = line[:-1] 2692 line = line[:-1]
2597 r.append(line) 2693 r.append(line)
2598 if p.Wait() != 0: 2694 if p.Wait() != 0:
2599 raise GitError('%s rev-list %s: %s' % ( 2695 raise GitError('%s rev-list %s: %s' %
2600 self._project.name, 2696 (self._project.name, str(args), p.stderr))
2601 str(args),
2602 p.stderr))
2603 return r 2697 return r
2604 2698
2605 def __getattr__(self, name): 2699 def __getattr__(self, name):
@@ -2622,6 +2716,7 @@ class Project(object):
2622 A callable object that will try to call git with the named command. 2716 A callable object that will try to call git with the named command.
2623 """ 2717 """
2624 name = name.replace('_', '-') 2718 name = name.replace('_', '-')
2719
2625 def runner(*args, **kwargs): 2720 def runner(*args, **kwargs):
2626 cmdv = [] 2721 cmdv = []
2627 config = kwargs.pop('config', None) 2722 config = kwargs.pop('config', None)
@@ -2644,10 +2739,8 @@ class Project(object):
2644 capture_stdout=True, 2739 capture_stdout=True,
2645 capture_stderr=True) 2740 capture_stderr=True)
2646 if p.Wait() != 0: 2741 if p.Wait() != 0:
2647 raise GitError('%s %s: %s' % ( 2742 raise GitError('%s %s: %s' %
2648 self._project.name, 2743 (self._project.name, name, p.stderr))
2649 name,
2650 p.stderr))
2651 r = p.stdout 2744 r = p.stdout
2652 try: 2745 try:
2653 r = r.decode('utf-8') 2746 r = r.decode('utf-8')
@@ -2660,14 +2753,19 @@ class Project(object):
2660 2753
2661 2754
2662class _PriorSyncFailedError(Exception): 2755class _PriorSyncFailedError(Exception):
2756
2663 def __str__(self): 2757 def __str__(self):
2664 return 'prior sync failed; rebase still in progress' 2758 return 'prior sync failed; rebase still in progress'
2665 2759
2760
2666class _DirtyError(Exception): 2761class _DirtyError(Exception):
2762
2667 def __str__(self): 2763 def __str__(self):
2668 return 'contains uncommitted changes' 2764 return 'contains uncommitted changes'
2669 2765
2766
2670class _InfoMessage(object): 2767class _InfoMessage(object):
2768
2671 def __init__(self, project, text): 2769 def __init__(self, project, text):
2672 self.project = project 2770 self.project = project
2673 self.text = text 2771 self.text = text
@@ -2676,7 +2774,9 @@ class _InfoMessage(object):
2676 syncbuf.out.info('%s/: %s', self.project.relpath, self.text) 2774 syncbuf.out.info('%s/: %s', self.project.relpath, self.text)
2677 syncbuf.out.nl() 2775 syncbuf.out.nl()
2678 2776
2777
2679class _Failure(object): 2778class _Failure(object):
2779
2680 def __init__(self, project, why): 2780 def __init__(self, project, why):
2681 self.project = project 2781 self.project = project
2682 self.why = why 2782 self.why = why
@@ -2687,7 +2787,9 @@ class _Failure(object):
2687 str(self.why)) 2787 str(self.why))
2688 syncbuf.out.nl() 2788 syncbuf.out.nl()
2689 2789
2790
2690class _Later(object): 2791class _Later(object):
2792
2691 def __init__(self, project, action): 2793 def __init__(self, project, action):
2692 self.project = project 2794 self.project = project
2693 self.action = action 2795 self.action = action
@@ -2704,14 +2806,18 @@ class _Later(object):
2704 out.nl() 2806 out.nl()
2705 return False 2807 return False
2706 2808
2809
2707class _SyncColoring(Coloring): 2810class _SyncColoring(Coloring):
2811
2708 def __init__(self, config): 2812 def __init__(self, config):
2709 Coloring.__init__(self, config, 'reposync') 2813 Coloring.__init__(self, config, 'reposync')
2710 self.project = self.printer('header', attr='bold') 2814 self.project = self.printer('header', attr='bold')
2711 self.info = self.printer('info') 2815 self.info = self.printer('info')
2712 self.fail = self.printer('fail', fg='red') 2816 self.fail = self.printer('fail', fg='red')
2713 2817
2818
2714class SyncBuffer(object): 2819class SyncBuffer(object):
2820
2715 def __init__(self, config, detach_head=False): 2821 def __init__(self, config, detach_head=False):
2716 self._messages = [] 2822 self._messages = []
2717 self._failures = [] 2823 self._failures = []
@@ -2767,8 +2873,10 @@ class SyncBuffer(object):
2767 2873
2768 2874
2769class MetaProject(Project): 2875class MetaProject(Project):
2876
2770 """A special project housed under .repo. 2877 """A special project housed under .repo.
2771 """ 2878 """
2879
2772 def __init__(self, manifest, name, gitdir, worktree): 2880 def __init__(self, manifest, name, gitdir, worktree):
2773 Project.__init__(self, 2881 Project.__init__(self,
2774 manifest=manifest, 2882 manifest=manifest,
@@ -2802,10 +2910,9 @@ class MetaProject(Project):
2802 syncbuf.Finish() 2910 syncbuf.Finish()
2803 2911
2804 return GitCommand(self, 2912 return GitCommand(self,
2805 ['update-ref', '-d', 'refs/heads/default'], 2913 ['update-ref', '-d', 'refs/heads/default'],
2806 capture_stdout=True, 2914 capture_stdout=True,
2807 capture_stderr=True).Wait() == 0 2915 capture_stderr=True).Wait() == 0
2808
2809 2916
2810 @property 2917 @property
2811 def LastFetch(self): 2918 def LastFetch(self):