summaryrefslogtreecommitdiffstats
path: root/project.py
diff options
context:
space:
mode:
Diffstat (limited to 'project.py')
-rw-r--r--project.py352
1 files changed, 188 insertions, 164 deletions
diff --git a/project.py b/project.py
index a117f4df..e3c3bd51 100644
--- a/project.py
+++ b/project.py
@@ -13,7 +13,6 @@
13# limitations under the License. 13# limitations under the License.
14 14
15from __future__ import print_function 15from __future__ import print_function
16import contextlib
17import errno 16import errno
18import filecmp 17import filecmp
19import glob 18import glob
@@ -31,7 +30,8 @@ import traceback
31 30
32from color import Coloring 31from color import Coloring
33from git_command import GitCommand, git_require 32from git_command import GitCommand, git_require
34from git_config import GitConfig, IsId, GetSchemeFromUrl, ID_RE 33from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \
34 ID_RE
35from error import GitError, HookError, UploadError, DownloadError 35from error import GitError, HookError, UploadError, DownloadError
36from error import ManifestInvalidRevisionError 36from error import ManifestInvalidRevisionError
37from error import NoManifestException 37from error import NoManifestException
@@ -45,6 +45,7 @@ if not is_python3():
45 input = raw_input 45 input = raw_input
46 # pylint:enable=W0622 46 # pylint:enable=W0622
47 47
48
48def _lwrite(path, content): 49def _lwrite(path, content):
49 lock = '%s.lock' % path 50 lock = '%s.lock' % path
50 51
@@ -60,17 +61,27 @@ def _lwrite(path, content):
60 os.remove(lock) 61 os.remove(lock)
61 raise 62 raise
62 63
64
63def _error(fmt, *args): 65def _error(fmt, *args):
64 msg = fmt % args 66 msg = fmt % args
65 print('error: %s' % msg, file=sys.stderr) 67 print('error: %s' % msg, file=sys.stderr)
66 68
69
70def _warn(fmt, *args):
71 msg = fmt % args
72 print('warn: %s' % msg, file=sys.stderr)
73
74
67def not_rev(r): 75def not_rev(r):
68 return '^' + r 76 return '^' + r
69 77
78
70def sq(r): 79def sq(r):
71 return "'" + r.replace("'", "'\''") + "'" 80 return "'" + r.replace("'", "'\''") + "'"
72 81
73_project_hook_list = None 82_project_hook_list = None
83
84
74def _ProjectHooks(): 85def _ProjectHooks():
75 """List the hooks present in the 'hooks' directory. 86 """List the hooks present in the 'hooks' directory.
76 87
@@ -104,15 +115,14 @@ class DownloadedChange(object):
104 @property 115 @property
105 def commits(self): 116 def commits(self):
106 if self._commit_cache is None: 117 if self._commit_cache is None:
107 self._commit_cache = self.project.bare_git.rev_list( 118 self._commit_cache = self.project.bare_git.rev_list('--abbrev=8',
108 '--abbrev=8', 119 '--abbrev-commit',
109 '--abbrev-commit', 120 '--pretty=oneline',
110 '--pretty=oneline', 121 '--reverse',
111 '--reverse', 122 '--date-order',
112 '--date-order', 123 not_rev(self.base),
113 not_rev(self.base), 124 self.commit,
114 self.commit, 125 '--')
115 '--')
116 return self._commit_cache 126 return self._commit_cache
117 127
118 128
@@ -131,36 +141,36 @@ class ReviewableBranch(object):
131 @property 141 @property
132 def commits(self): 142 def commits(self):
133 if self._commit_cache is None: 143 if self._commit_cache is None:
134 self._commit_cache = self.project.bare_git.rev_list( 144 self._commit_cache = self.project.bare_git.rev_list('--abbrev=8',
135 '--abbrev=8', 145 '--abbrev-commit',
136 '--abbrev-commit', 146 '--pretty=oneline',
137 '--pretty=oneline', 147 '--reverse',
138 '--reverse', 148 '--date-order',
139 '--date-order', 149 not_rev(self.base),
140 not_rev(self.base), 150 R_HEADS + self.name,
141 R_HEADS + self.name, 151 '--')
142 '--')
143 return self._commit_cache 152 return self._commit_cache
144 153
145 @property 154 @property
146 def unabbrev_commits(self): 155 def unabbrev_commits(self):
147 r = dict() 156 r = dict()
148 for commit in self.project.bare_git.rev_list( 157 for commit in self.project.bare_git.rev_list(not_rev(self.base),
149 not_rev(self.base), 158 R_HEADS + self.name,
150 R_HEADS + self.name, 159 '--'):
151 '--'):
152 r[commit[0:8]] = commit 160 r[commit[0:8]] = commit
153 return r 161 return r
154 162
155 @property 163 @property
156 def date(self): 164 def date(self):
157 return self.project.bare_git.log( 165 return self.project.bare_git.log('--pretty=format:%cd',
158 '--pretty=format:%cd', 166 '-n', '1',
159 '-n', '1', 167 R_HEADS + self.name,
160 R_HEADS + self.name, 168 '--')
161 '--')
162 169
163 def UploadForReview(self, people, auto_topic=False, draft=False, dest_branch=None): 170 def UploadForReview(self, people,
171 auto_topic=False,
172 draft=False,
173 dest_branch=None):
164 self.project.UploadForReview(self.name, 174 self.project.UploadForReview(self.name,
165 people, 175 people,
166 auto_topic=auto_topic, 176 auto_topic=auto_topic,
@@ -170,8 +180,8 @@ class ReviewableBranch(object):
170 def GetPublishedRefs(self): 180 def GetPublishedRefs(self):
171 refs = {} 181 refs = {}
172 output = self.project.bare_git.ls_remote( 182 output = self.project.bare_git.ls_remote(
173 self.branch.remote.SshReviewUrl(self.project.UserEmail), 183 self.branch.remote.SshReviewUrl(self.project.UserEmail),
174 'refs/changes/*') 184 'refs/changes/*')
175 for line in output.split('\n'): 185 for line in output.split('\n'):
176 try: 186 try:
177 (sha, ref) = line.split() 187 (sha, ref) = line.split()
@@ -181,7 +191,9 @@ class ReviewableBranch(object):
181 191
182 return refs 192 return refs
183 193
194
184class StatusColoring(Coloring): 195class StatusColoring(Coloring):
196
185 def __init__(self, config): 197 def __init__(self, config):
186 Coloring.__init__(self, config, 'status') 198 Coloring.__init__(self, config, 'status')
187 self.project = self.printer('header', attr='bold') 199 self.project = self.printer('header', attr='bold')
@@ -195,17 +207,22 @@ class StatusColoring(Coloring):
195 207
196 208
197class DiffColoring(Coloring): 209class DiffColoring(Coloring):
210
198 def __init__(self, config): 211 def __init__(self, config):
199 Coloring.__init__(self, config, 'diff') 212 Coloring.__init__(self, config, 'diff')
200 self.project = self.printer('header', attr='bold') 213 self.project = self.printer('header', attr='bold')
201 214
215
202class _Annotation(object): 216class _Annotation(object):
217
203 def __init__(self, name, value, keep): 218 def __init__(self, name, value, keep):
204 self.name = name 219 self.name = name
205 self.value = value 220 self.value = value
206 self.keep = keep 221 self.keep = keep
207 222
223
208class _CopyFile(object): 224class _CopyFile(object):
225
209 def __init__(self, src, dest, abssrc, absdest): 226 def __init__(self, src, dest, abssrc, absdest):
210 self.src = src 227 self.src = src
211 self.dest = dest 228 self.dest = dest
@@ -233,7 +250,9 @@ class _CopyFile(object):
233 except IOError: 250 except IOError:
234 _error('Cannot copy file %s to %s', src, dest) 251 _error('Cannot copy file %s to %s', src, dest)
235 252
253
236class _LinkFile(object): 254class _LinkFile(object):
255
237 def __init__(self, git_worktree, src, dest, relsrc, absdest): 256 def __init__(self, git_worktree, src, dest, relsrc, absdest):
238 self.git_worktree = git_worktree 257 self.git_worktree = git_worktree
239 self.src = src 258 self.src = src
@@ -246,7 +265,7 @@ class _LinkFile(object):
246 if not os.path.islink(absDest) or (os.readlink(absDest) != relSrc): 265 if not os.path.islink(absDest) or (os.readlink(absDest) != relSrc):
247 try: 266 try:
248 # remove existing file first, since it might be read-only 267 # remove existing file first, since it might be read-only
249 if os.path.exists(absDest): 268 if os.path.lexists(absDest):
250 os.remove(absDest) 269 os.remove(absDest)
251 else: 270 else:
252 dest_dir = os.path.dirname(absDest) 271 dest_dir = os.path.dirname(absDest)
@@ -272,7 +291,7 @@ class _LinkFile(object):
272 absDestDir = self.abs_dest 291 absDestDir = self.abs_dest
273 if os.path.exists(absDestDir) and not os.path.isdir(absDestDir): 292 if os.path.exists(absDestDir) and not os.path.isdir(absDestDir):
274 _error('Link error: src with wildcard, %s must be a directory', 293 _error('Link error: src with wildcard, %s must be a directory',
275 absDestDir) 294 absDestDir)
276 else: 295 else:
277 absSrcFiles = glob.glob(absSrc) 296 absSrcFiles = glob.glob(absSrc)
278 for absSrcFile in absSrcFiles: 297 for absSrcFile in absSrcFiles:
@@ -289,7 +308,9 @@ class _LinkFile(object):
289 relSrc = os.path.join(relSrcDir, srcFile) 308 relSrc = os.path.join(relSrcDir, srcFile)
290 self.__linkIt(relSrc, absDest) 309 self.__linkIt(relSrc, absDest)
291 310
311
292class RemoteSpec(object): 312class RemoteSpec(object):
313
293 def __init__(self, 314 def __init__(self,
294 name, 315 name,
295 url=None, 316 url=None,
@@ -300,7 +321,9 @@ class RemoteSpec(object):
300 self.review = review 321 self.review = review
301 self.revision = revision 322 self.revision = revision
302 323
324
303class RepoHook(object): 325class RepoHook(object):
326
304 """A RepoHook contains information about a script to run as a hook. 327 """A RepoHook contains information about a script to run as a hook.
305 328
306 Hooks are used to run a python script before running an upload (for instance, 329 Hooks are used to run a python script before running an upload (for instance,
@@ -313,6 +336,7 @@ class RepoHook(object):
313 Hooks are always python. When a hook is run, we will load the hook into the 336 Hooks are always python. When a hook is run, we will load the hook into the
314 interpreter and execute its main() function. 337 interpreter and execute its main() function.
315 """ 338 """
339
316 def __init__(self, 340 def __init__(self,
317 hook_type, 341 hook_type,
318 hooks_project, 342 hooks_project,
@@ -427,8 +451,8 @@ class RepoHook(object):
427 ' %s\n' 451 ' %s\n'
428 '\n' 452 '\n'
429 'Do you want to allow this script to run ' 453 'Do you want to allow this script to run '
430 '(yes/yes-never-ask-again/NO)? ') % ( 454 '(yes/yes-never-ask-again/NO)? ') % (self._GetMustVerb(),
431 self._GetMustVerb(), self._script_fullpath) 455 self._script_fullpath)
432 response = input(prompt).lower() 456 response = input(prompt).lower()
433 print() 457 print()
434 458
@@ -472,19 +496,18 @@ class RepoHook(object):
472 496
473 # Exec, storing global context in the context dict. We catch exceptions 497 # Exec, storing global context in the context dict. We catch exceptions
474 # and convert to a HookError w/ just the failing traceback. 498 # and convert to a HookError w/ just the failing traceback.
475 context = {} 499 context = {'__file__': self._script_fullpath}
476 try: 500 try:
477 exec(compile(open(self._script_fullpath).read(), 501 exec(compile(open(self._script_fullpath).read(),
478 self._script_fullpath, 'exec'), context) 502 self._script_fullpath, 'exec'), context)
479 except Exception: 503 except Exception:
480 raise HookError('%s\nFailed to import %s hook; see traceback above.' % ( 504 raise HookError('%s\nFailed to import %s hook; see traceback above.' %
481 traceback.format_exc(), self._hook_type)) 505 (traceback.format_exc(), self._hook_type))
482 506
483 # Running the script should have defined a main() function. 507 # Running the script should have defined a main() function.
484 if 'main' not in context: 508 if 'main' not in context:
485 raise HookError('Missing main() in: "%s"' % self._script_fullpath) 509 raise HookError('Missing main() in: "%s"' % self._script_fullpath)
486 510
487
488 # Add 'hook_should_take_kwargs' to the arguments to be passed to main. 511 # Add 'hook_should_take_kwargs' to the arguments to be passed to main.
489 # We don't actually want hooks to define their main with this argument-- 512 # We don't actually want hooks to define their main with this argument--
490 # it's there to remind them that their hook should always take **kwargs. 513 # it's there to remind them that their hook should always take **kwargs.
@@ -502,8 +525,8 @@ class RepoHook(object):
502 context['main'](**kwargs) 525 context['main'](**kwargs)
503 except Exception: 526 except Exception:
504 raise HookError('%s\nFailed to run main() for %s hook; see traceback ' 527 raise HookError('%s\nFailed to run main() for %s hook; see traceback '
505 'above.' % ( 528 'above.' % (traceback.format_exc(),
506 traceback.format_exc(), self._hook_type)) 529 self._hook_type))
507 finally: 530 finally:
508 # Restore sys.path and CWD. 531 # Restore sys.path and CWD.
509 sys.path = orig_syspath 532 sys.path = orig_syspath
@@ -527,8 +550,8 @@ class RepoHook(object):
527 to run a required hook (from _CheckForHookApproval). 550 to run a required hook (from _CheckForHookApproval).
528 """ 551 """
529 # No-op if there is no hooks project or if hook is disabled. 552 # No-op if there is no hooks project or if hook is disabled.
530 if ((not self._hooks_project) or 553 if ((not self._hooks_project) or (self._hook_type not in
531 (self._hook_type not in self._hooks_project.enabled_repo_hooks)): 554 self._hooks_project.enabled_repo_hooks)):
532 return 555 return
533 556
534 # Bail with a nice error if we can't find the hook. 557 # Bail with a nice error if we can't find the hook.
@@ -550,6 +573,7 @@ class Project(object):
550 # These objects can only be used by a single working tree. 573 # These objects can only be used by a single working tree.
551 working_tree_files = ['config', 'packed-refs', 'shallow'] 574 working_tree_files = ['config', 'packed-refs', 'shallow']
552 working_tree_dirs = ['logs', 'refs'] 575 working_tree_dirs = ['logs', 'refs']
576
553 def __init__(self, 577 def __init__(self,
554 manifest, 578 manifest,
555 name, 579 name,
@@ -569,7 +593,8 @@ class Project(object):
569 parent=None, 593 parent=None,
570 is_derived=False, 594 is_derived=False,
571 dest_branch=None, 595 dest_branch=None,
572 optimized_fetch=False): 596 optimized_fetch=False,
597 old_revision=None):
573 """Init a Project object. 598 """Init a Project object.
574 599
575 Args: 600 Args:
@@ -593,6 +618,7 @@ class Project(object):
593 dest_branch: The branch to which to push changes for review by default. 618 dest_branch: The branch to which to push changes for review by default.
594 optimized_fetch: If True, when a project is set to a sha1 revision, only 619 optimized_fetch: If True, when a project is set to a sha1 revision, only
595 fetch from the remote if the sha1 is not present locally. 620 fetch from the remote if the sha1 is not present locally.
621 old_revision: saved git commit id for open GITC projects.
596 """ 622 """
597 self.manifest = manifest 623 self.manifest = manifest
598 self.name = name 624 self.name = name
@@ -606,9 +632,9 @@ class Project(object):
606 self.relpath = relpath 632 self.relpath = relpath
607 self.revisionExpr = revisionExpr 633 self.revisionExpr = revisionExpr
608 634
609 if revisionId is None \ 635 if revisionId is None \
610 and revisionExpr \ 636 and revisionExpr \
611 and IsId(revisionExpr): 637 and IsId(revisionExpr):
612 self.revisionId = revisionExpr 638 self.revisionId = revisionExpr
613 else: 639 else:
614 self.revisionId = revisionId 640 self.revisionId = revisionId
@@ -628,9 +654,8 @@ class Project(object):
628 self.copyfiles = [] 654 self.copyfiles = []
629 self.linkfiles = [] 655 self.linkfiles = []
630 self.annotations = [] 656 self.annotations = []
631 self.config = GitConfig.ForRepository( 657 self.config = GitConfig.ForRepository(gitdir=self.gitdir,
632 gitdir=self.gitdir, 658 defaults=self.manifest.globalConfig)
633 defaults=self.manifest.globalConfig)
634 659
635 if self.worktree: 660 if self.worktree:
636 self.work_git = self._GitGetByExec(self, bare=False, gitdir=gitdir) 661 self.work_git = self._GitGetByExec(self, bare=False, gitdir=gitdir)
@@ -640,6 +665,7 @@ class Project(object):
640 self.bare_ref = GitRefs(gitdir) 665 self.bare_ref = GitRefs(gitdir)
641 self.bare_objdir = self._GitGetByExec(self, bare=True, gitdir=objdir) 666 self.bare_objdir = self._GitGetByExec(self, bare=True, gitdir=objdir)
642 self.dest_branch = dest_branch 667 self.dest_branch = dest_branch
668 self.old_revision = old_revision
643 669
644 # This will be filled in if a project is later identified to be the 670 # This will be filled in if a project is later identified to be the
645 # project containing repo hooks. 671 # project containing repo hooks.
@@ -767,7 +793,7 @@ class Project(object):
767 """ 793 """
768 expanded_manifest_groups = manifest_groups or ['default'] 794 expanded_manifest_groups = manifest_groups or ['default']
769 expanded_project_groups = ['all'] + (self.groups or []) 795 expanded_project_groups = ['all'] + (self.groups or [])
770 if not 'notdefault' in expanded_project_groups: 796 if 'notdefault' not in expanded_project_groups:
771 expanded_project_groups += ['default'] 797 expanded_project_groups += ['default']
772 798
773 matched = False 799 matched = False
@@ -779,7 +805,7 @@ class Project(object):
779 805
780 return matched 806 return matched
781 807
782## Status Display ## 808# Status Display ##
783 def UncommitedFiles(self, get_all=True): 809 def UncommitedFiles(self, get_all=True):
784 """Returns a list of strings, uncommitted files in the git tree. 810 """Returns a list of strings, uncommitted files in the git tree.
785 811
@@ -831,7 +857,7 @@ class Project(object):
831 output: If specified, redirect the output to this object. 857 output: If specified, redirect the output to this object.
832 """ 858 """
833 if not os.path.isdir(self.worktree): 859 if not os.path.isdir(self.worktree):
834 if output_redir == None: 860 if output_redir is None:
835 output_redir = sys.stdout 861 output_redir = sys.stdout
836 print(file=output_redir) 862 print(file=output_redir)
837 print('project %s/' % self.relpath, file=output_redir) 863 print('project %s/' % self.relpath, file=output_redir)
@@ -850,7 +876,7 @@ class Project(object):
850 return 'CLEAN' 876 return 'CLEAN'
851 877
852 out = StatusColoring(self.config) 878 out = StatusColoring(self.config)
853 if not output_redir == None: 879 if output_redir is not None:
854 out.redirect(output_redir) 880 out.redirect(output_redir)
855 out.project('project %-40s', self.relpath + '/ ') 881 out.project('project %-40s', self.relpath + '/ ')
856 882
@@ -893,7 +919,7 @@ class Project(object):
893 919
894 if i and i.src_path: 920 if i and i.src_path:
895 line = ' %s%s\t%s => %s (%s%%)' % (i_status, f_status, 921 line = ' %s%s\t%s => %s (%s%%)' % (i_status, f_status,
896 i.src_path, p, i.level) 922 i.src_path, p, i.level)
897 else: 923 else:
898 line = ' %s%s\t%s' % (i_status, f_status, p) 924 line = ' %s%s\t%s' % (i_status, f_status, p)
899 925
@@ -936,7 +962,7 @@ class Project(object):
936 p.Wait() 962 p.Wait()
937 963
938 964
939## Publish / Upload ## 965# Publish / Upload ##
940 966
941 def WasPublished(self, branch, all_refs=None): 967 def WasPublished(self, branch, all_refs=None):
942 """Was the branch published (uploaded) for code review? 968 """Was the branch published (uploaded) for code review?
@@ -1079,7 +1105,7 @@ class Project(object):
1079 message=msg) 1105 message=msg)
1080 1106
1081 1107
1082## Sync ## 1108# Sync ##
1083 1109
1084 def _ExtractArchive(self, tarpath, path=None): 1110 def _ExtractArchive(self, tarpath, path=None):
1085 """Extract the given tar on its current location 1111 """Extract the given tar on its current location
@@ -1093,26 +1119,25 @@ class Project(object):
1093 tar.extractall(path=path) 1119 tar.extractall(path=path)
1094 return True 1120 return True
1095 except (IOError, tarfile.TarError) as e: 1121 except (IOError, tarfile.TarError) as e:
1096 print("error: Cannot extract archive %s: " 1122 _error("Cannot extract archive %s: %s", tarpath, str(e))
1097 "%s" % (tarpath, str(e)), file=sys.stderr)
1098 return False 1123 return False
1099 1124
1100 def Sync_NetworkHalf(self, 1125 def Sync_NetworkHalf(self,
1101 quiet=False, 1126 quiet=False,
1102 is_new=None, 1127 is_new=None,
1103 current_branch_only=False, 1128 current_branch_only=False,
1104 force_sync=False, 1129 force_sync=False,
1105 clone_bundle=True, 1130 clone_bundle=True,
1106 no_tags=False, 1131 no_tags=False,
1107 archive=False, 1132 archive=False,
1108 optimized_fetch=False): 1133 optimized_fetch=False,
1134 prune=False):
1109 """Perform only the network IO portion of the sync process. 1135 """Perform only the network IO portion of the sync process.
1110 Local working directory/branch state is not affected. 1136 Local working directory/branch state is not affected.
1111 """ 1137 """
1112 if archive and not isinstance(self, MetaProject): 1138 if archive and not isinstance(self, MetaProject):
1113 if self.remote.url.startswith(('http://', 'https://')): 1139 if self.remote.url.startswith(('http://', 'https://')):
1114 print("error: %s: Cannot fetch archives from http/https " 1140 _error("%s: Cannot fetch archives from http/https remotes.", self.name)
1115 "remotes." % self.name, file=sys.stderr)
1116 return False 1141 return False
1117 1142
1118 name = self.relpath.replace('\\', '/') 1143 name = self.relpath.replace('\\', '/')
@@ -1123,7 +1148,7 @@ class Project(object):
1123 try: 1148 try:
1124 self._FetchArchive(tarpath, cwd=topdir) 1149 self._FetchArchive(tarpath, cwd=topdir)
1125 except GitError as e: 1150 except GitError as e:
1126 print('error: %s' % str(e), file=sys.stderr) 1151 _error('%s', e)
1127 return False 1152 return False
1128 1153
1129 # From now on, we only need absolute tarpath 1154 # From now on, we only need absolute tarpath
@@ -1134,8 +1159,7 @@ class Project(object):
1134 try: 1159 try:
1135 os.remove(tarpath) 1160 os.remove(tarpath)
1136 except OSError as e: 1161 except OSError as e:
1137 print("warn: Cannot remove archive %s: " 1162 _warn("Cannot remove archive %s: %s", tarpath, str(e))
1138 "%s" % (tarpath, str(e)), file=sys.stderr)
1139 self._CopyAndLinkFiles() 1163 self._CopyAndLinkFiles()
1140 return True 1164 return True
1141 if is_new is None: 1165 if is_new is None:
@@ -1160,8 +1184,8 @@ class Project(object):
1160 alt_dir = None 1184 alt_dir = None
1161 1185
1162 if clone_bundle \ 1186 if clone_bundle \
1163 and alt_dir is None \ 1187 and alt_dir is None \
1164 and self._ApplyCloneBundle(initial=is_new, quiet=quiet): 1188 and self._ApplyCloneBundle(initial=is_new, quiet=quiet):
1165 is_new = False 1189 is_new = False
1166 1190
1167 if not current_branch_only: 1191 if not current_branch_only:
@@ -1173,12 +1197,13 @@ class Project(object):
1173 elif self.manifest.default.sync_c: 1197 elif self.manifest.default.sync_c:
1174 current_branch_only = True 1198 current_branch_only = True
1175 1199
1176 need_to_fetch = not (optimized_fetch and \ 1200 need_to_fetch = not (optimized_fetch and
1177 (ID_RE.match(self.revisionExpr) and self._CheckForSha1())) 1201 (ID_RE.match(self.revisionExpr) and
1178 if (need_to_fetch 1202 self._CheckForSha1()))
1179 and not self._RemoteFetch(initial=is_new, quiet=quiet, alt_dir=alt_dir, 1203 if (need_to_fetch and
1180 current_branch_only=current_branch_only, 1204 not self._RemoteFetch(initial=is_new, quiet=quiet, alt_dir=alt_dir,
1181 no_tags=no_tags)): 1205 current_branch_only=current_branch_only,
1206 no_tags=no_tags, prune=prune)):
1182 return False 1207 return False
1183 1208
1184 if self.worktree: 1209 if self.worktree:
@@ -1195,6 +1220,8 @@ class Project(object):
1195 self._InitHooks() 1220 self._InitHooks()
1196 1221
1197 def _CopyAndLinkFiles(self): 1222 def _CopyAndLinkFiles(self):
1223 if self.manifest.isGitcClient:
1224 return
1198 for copyfile in self.copyfiles: 1225 for copyfile in self.copyfiles:
1199 copyfile._Copy() 1226 copyfile._Copy()
1200 for linkfile in self.linkfiles: 1227 for linkfile in self.linkfiles:
@@ -1213,9 +1240,8 @@ class Project(object):
1213 try: 1240 try:
1214 return self.bare_git.rev_list(self.revisionExpr, '-1')[0] 1241 return self.bare_git.rev_list(self.revisionExpr, '-1')[0]
1215 except GitError: 1242 except GitError:
1216 raise ManifestInvalidRevisionError( 1243 raise ManifestInvalidRevisionError('revision %s in %s not found' %
1217 'revision %s in %s not found' % (self.revisionExpr, 1244 (self.revisionExpr, self.name))
1218 self.name))
1219 1245
1220 def GetRevisionId(self, all_refs=None): 1246 def GetRevisionId(self, all_refs=None):
1221 if self.revisionId: 1247 if self.revisionId:
@@ -1230,9 +1256,8 @@ class Project(object):
1230 try: 1256 try:
1231 return self.bare_git.rev_parse('--verify', '%s^0' % rev) 1257 return self.bare_git.rev_parse('--verify', '%s^0' % rev)
1232 except GitError: 1258 except GitError:
1233 raise ManifestInvalidRevisionError( 1259 raise ManifestInvalidRevisionError('revision %s in %s not found' %
1234 'revision %s in %s not found' % (self.revisionExpr, 1260 (self.revisionExpr, self.name))
1235 self.name))
1236 1261
1237 def Sync_LocalHalf(self, syncbuf, force_sync=False): 1262 def Sync_LocalHalf(self, syncbuf, force_sync=False):
1238 """Perform only the local IO portion of the sync process. 1263 """Perform only the local IO portion of the sync process.
@@ -1270,6 +1295,8 @@ class Project(object):
1270 # Except if the head needs to be detached 1295 # Except if the head needs to be detached
1271 # 1296 #
1272 if not syncbuf.detach_head: 1297 if not syncbuf.detach_head:
1298 # The copy/linkfile config may have changed.
1299 self._CopyAndLinkFiles()
1273 return 1300 return
1274 else: 1301 else:
1275 lost = self._revlist(not_rev(revid), HEAD) 1302 lost = self._revlist(not_rev(revid), HEAD)
@@ -1287,6 +1314,8 @@ class Project(object):
1287 if head == revid: 1314 if head == revid:
1288 # No changes; don't do anything further. 1315 # No changes; don't do anything further.
1289 # 1316 #
1317 # The copy/linkfile config may have changed.
1318 self._CopyAndLinkFiles()
1290 return 1319 return
1291 1320
1292 branch = self.GetBranch(branch) 1321 branch = self.GetBranch(branch)
@@ -1317,8 +1346,8 @@ class Project(object):
1317 # to rewrite the published commits so we punt. 1346 # to rewrite the published commits so we punt.
1318 # 1347 #
1319 syncbuf.fail(self, 1348 syncbuf.fail(self,
1320 "branch %s is published (but not merged) and is now %d commits behind" 1349 "branch %s is published (but not merged) and is now "
1321 % (branch.name, len(upstream_gain))) 1350 "%d commits behind" % (branch.name, len(upstream_gain)))
1322 return 1351 return
1323 elif pub == head: 1352 elif pub == head:
1324 # All published commits are merged, and thus we are a 1353 # All published commits are merged, and thus we are a
@@ -1412,7 +1441,7 @@ class Project(object):
1412 remote = self.GetRemote(self.remote.name) 1441 remote = self.GetRemote(self.remote.name)
1413 1442
1414 cmd = ['fetch', remote.name] 1443 cmd = ['fetch', remote.name]
1415 cmd.append('refs/changes/%2.2d/%d/%d' \ 1444 cmd.append('refs/changes/%2.2d/%d/%d'
1416 % (change_id % 100, change_id, patch_id)) 1445 % (change_id % 100, change_id, patch_id))
1417 if GitCommand(self, cmd, bare=True).Wait() != 0: 1446 if GitCommand(self, cmd, bare=True).Wait() != 0:
1418 return None 1447 return None
@@ -1423,11 +1452,13 @@ class Project(object):
1423 self.bare_git.rev_parse('FETCH_HEAD')) 1452 self.bare_git.rev_parse('FETCH_HEAD'))
1424 1453
1425 1454
1426## Branch Management ## 1455# Branch Management ##
1427 1456
1428 def StartBranch(self, name): 1457 def StartBranch(self, name, branch_merge=''):
1429 """Create a new branch off the manifest's revision. 1458 """Create a new branch off the manifest's revision.
1430 """ 1459 """
1460 if not branch_merge:
1461 branch_merge = self.revisionExpr
1431 head = self.work_git.GetHead() 1462 head = self.work_git.GetHead()
1432 if head == (R_HEADS + name): 1463 if head == (R_HEADS + name):
1433 return True 1464 return True
@@ -1441,9 +1472,9 @@ class Project(object):
1441 1472
1442 branch = self.GetBranch(name) 1473 branch = self.GetBranch(name)
1443 branch.remote = self.GetRemote(self.remote.name) 1474 branch.remote = self.GetRemote(self.remote.name)
1444 branch.merge = self.revisionExpr 1475 branch.merge = branch_merge
1445 if not branch.merge.startswith('refs/') and not ID_RE.match(self.revisionExpr): 1476 if not branch.merge.startswith('refs/') and not ID_RE.match(branch_merge):
1446 branch.merge = R_HEADS + self.revisionExpr 1477 branch.merge = R_HEADS + branch_merge
1447 revid = self.GetRevisionId(all_refs) 1478 revid = self.GetRevisionId(all_refs)
1448 1479
1449 if head.startswith(R_HEADS): 1480 if head.startswith(R_HEADS):
@@ -1451,7 +1482,6 @@ class Project(object):
1451 head = all_refs[head] 1482 head = all_refs[head]
1452 except KeyError: 1483 except KeyError:
1453 head = None 1484 head = None
1454
1455 if revid and head and revid == head: 1485 if revid and head and revid == head:
1456 ref = os.path.join(self.gitdir, R_HEADS + name) 1486 ref = os.path.join(self.gitdir, R_HEADS + name)
1457 try: 1487 try:
@@ -1572,8 +1602,6 @@ class Project(object):
1572 1602
1573 if kill: 1603 if kill:
1574 old = self.bare_git.GetHead() 1604 old = self.bare_git.GetHead()
1575 if old is None:
1576 old = 'refs/heads/please_never_use_this_as_a_branch_name'
1577 1605
1578 try: 1606 try:
1579 self.bare_git.DetachHead(rev) 1607 self.bare_git.DetachHead(rev)
@@ -1585,7 +1613,10 @@ class Project(object):
1585 capture_stderr=True) 1613 capture_stderr=True)
1586 b.Wait() 1614 b.Wait()
1587 finally: 1615 finally:
1588 self.bare_git.SetHead(old) 1616 if ID_RE.match(old):
1617 self.bare_git.DetachHead(old)
1618 else:
1619 self.bare_git.SetHead(old)
1589 left = self._allrefs 1620 left = self._allrefs
1590 1621
1591 for branch in kill: 1622 for branch in kill:
@@ -1608,10 +1639,11 @@ class Project(object):
1608 return kept 1639 return kept
1609 1640
1610 1641
1611## Submodule Management ## 1642# Submodule Management ##
1612 1643
1613 def GetRegisteredSubprojects(self): 1644 def GetRegisteredSubprojects(self):
1614 result = [] 1645 result = []
1646
1615 def rec(subprojects): 1647 def rec(subprojects):
1616 if not subprojects: 1648 if not subprojects:
1617 return 1649 return
@@ -1646,6 +1678,7 @@ class Project(object):
1646 1678
1647 re_path = re.compile(r'^submodule\.([^.]+)\.path=(.*)$') 1679 re_path = re.compile(r'^submodule\.([^.]+)\.path=(.*)$')
1648 re_url = re.compile(r'^submodule\.([^.]+)\.url=(.*)$') 1680 re_url = re.compile(r'^submodule\.([^.]+)\.url=(.*)$')
1681
1649 def parse_gitmodules(gitdir, rev): 1682 def parse_gitmodules(gitdir, rev):
1650 cmd = ['cat-file', 'blob', '%s:.gitmodules' % rev] 1683 cmd = ['cat-file', 'blob', '%s:.gitmodules' % rev]
1651 try: 1684 try:
@@ -1755,7 +1788,7 @@ class Project(object):
1755 return result 1788 return result
1756 1789
1757 1790
1758## Direct Git Commands ## 1791# Direct Git Commands ##
1759 def _CheckForSha1(self): 1792 def _CheckForSha1(self):
1760 try: 1793 try:
1761 # if revision (sha or tag) is not present then following function 1794 # if revision (sha or tag) is not present then following function
@@ -1779,13 +1812,13 @@ class Project(object):
1779 if command.Wait() != 0: 1812 if command.Wait() != 0:
1780 raise GitError('git archive %s: %s' % (self.name, command.stderr)) 1813 raise GitError('git archive %s: %s' % (self.name, command.stderr))
1781 1814
1782
1783 def _RemoteFetch(self, name=None, 1815 def _RemoteFetch(self, name=None,
1784 current_branch_only=False, 1816 current_branch_only=False,
1785 initial=False, 1817 initial=False,
1786 quiet=False, 1818 quiet=False,
1787 alt_dir=None, 1819 alt_dir=None,
1788 no_tags=False): 1820 no_tags=False,
1821 prune=False):
1789 1822
1790 is_sha1 = False 1823 is_sha1 = False
1791 tag_name = None 1824 tag_name = None
@@ -1898,6 +1931,9 @@ class Project(object):
1898 else: 1931 else:
1899 cmd.append('--tags') 1932 cmd.append('--tags')
1900 1933
1934 if prune:
1935 cmd.append('--prune')
1936
1901 spec = [] 1937 spec = []
1902 if not current_branch_only: 1938 if not current_branch_only:
1903 # Fetch whole repo 1939 # Fetch whole repo
@@ -1939,9 +1975,9 @@ class Project(object):
1939 break 1975 break
1940 continue 1976 continue
1941 elif current_branch_only and is_sha1 and ret == 128: 1977 elif current_branch_only and is_sha1 and ret == 128:
1942 # Exit code 128 means "couldn't find the ref you asked for"; if we're in sha1 1978 # Exit code 128 means "couldn't find the ref you asked for"; if we're
1943 # mode, we just tried sync'ing from the upstream field; it doesn't exist, thus 1979 # in sha1 mode, we just tried sync'ing from the upstream field; it
1944 # abort the optimization attempt and do a full sync. 1980 # doesn't exist, thus abort the optimization attempt and do a full sync.
1945 break 1981 break
1946 elif ret < 0: 1982 elif ret < 0:
1947 # Git died with a signal, exit immediately 1983 # Git died with a signal, exit immediately
@@ -1968,20 +2004,24 @@ class Project(object):
1968 initial=False, quiet=quiet, alt_dir=alt_dir) 2004 initial=False, quiet=quiet, alt_dir=alt_dir)
1969 if self.clone_depth: 2005 if self.clone_depth:
1970 self.clone_depth = None 2006 self.clone_depth = None
1971 return self._RemoteFetch(name=name, current_branch_only=current_branch_only, 2007 return self._RemoteFetch(name=name,
2008 current_branch_only=current_branch_only,
1972 initial=False, quiet=quiet, alt_dir=alt_dir) 2009 initial=False, quiet=quiet, alt_dir=alt_dir)
1973 2010
1974 return ok 2011 return ok
1975 2012
1976 def _ApplyCloneBundle(self, initial=False, quiet=False): 2013 def _ApplyCloneBundle(self, initial=False, quiet=False):
1977 if initial and (self.manifest.manifestProject.config.GetString('repo.depth') or self.clone_depth): 2014 if initial and \
2015 (self.manifest.manifestProject.config.GetString('repo.depth') or
2016 self.clone_depth):
1978 return False 2017 return False
1979 2018
1980 remote = self.GetRemote(self.remote.name) 2019 remote = self.GetRemote(self.remote.name)
1981 bundle_url = remote.url + '/clone.bundle' 2020 bundle_url = remote.url + '/clone.bundle'
1982 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url) 2021 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
1983 if GetSchemeFromUrl(bundle_url) not in ( 2022 if GetSchemeFromUrl(bundle_url) not in ('http', 'https',
1984 'http', 'https', 'persistent-http', 'persistent-https'): 2023 'persistent-http',
2024 'persistent-https'):
1985 return False 2025 return False
1986 2026
1987 bundle_dst = os.path.join(self.gitdir, 'clone.bundle') 2027 bundle_dst = os.path.join(self.gitdir, 'clone.bundle')
@@ -2030,7 +2070,7 @@ class Project(object):
2030 os.remove(tmpPath) 2070 os.remove(tmpPath)
2031 if 'http_proxy' in os.environ and 'darwin' == sys.platform: 2071 if 'http_proxy' in os.environ and 'darwin' == sys.platform:
2032 cmd += ['--proxy', os.environ['http_proxy']] 2072 cmd += ['--proxy', os.environ['http_proxy']]
2033 with self._GetBundleCookieFile(srcUrl, quiet) as cookiefile: 2073 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, _proxy):
2034 if cookiefile: 2074 if cookiefile:
2035 cmd += ['--cookie', cookiefile, '--cookie-jar', cookiefile] 2075 cmd += ['--cookie', cookiefile, '--cookie-jar', cookiefile]
2036 if srcUrl.startswith('persistent-'): 2076 if srcUrl.startswith('persistent-'):
@@ -2078,40 +2118,6 @@ class Project(object):
2078 except OSError: 2118 except OSError:
2079 return False 2119 return False
2080 2120
2081 @contextlib.contextmanager
2082 def _GetBundleCookieFile(self, url, quiet):
2083 if url.startswith('persistent-'):
2084 try:
2085 p = subprocess.Popen(
2086 ['git-remote-persistent-https', '-print_config', url],
2087 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2088 stderr=subprocess.PIPE)
2089 try:
2090 prefix = 'http.cookiefile='
2091 cookiefile = None
2092 for line in p.stdout:
2093 line = line.strip()
2094 if line.startswith(prefix):
2095 cookiefile = line[len(prefix):]
2096 break
2097 # Leave subprocess open, as cookie file may be transient.
2098 if cookiefile:
2099 yield cookiefile
2100 return
2101 finally:
2102 p.stdin.close()
2103 if p.wait():
2104 err_msg = p.stderr.read()
2105 if ' -print_config' in err_msg:
2106 pass # Persistent proxy doesn't support -print_config.
2107 elif not quiet:
2108 print(err_msg, file=sys.stderr)
2109 except OSError as e:
2110 if e.errno == errno.ENOENT:
2111 pass # No persistent proxy.
2112 raise
2113 yield GitConfig.ForUser().GetString('http.cookiefile')
2114
2115 def _Checkout(self, rev, quiet=False): 2121 def _Checkout(self, rev, quiet=False):
2116 cmd = ['checkout'] 2122 cmd = ['checkout']
2117 if quiet: 2123 if quiet:
@@ -2182,12 +2188,13 @@ class Project(object):
2182 try: 2188 try:
2183 self._CheckDirReference(self.objdir, self.gitdir, share_refs=False) 2189 self._CheckDirReference(self.objdir, self.gitdir, share_refs=False)
2184 except GitError as e: 2190 except GitError as e:
2185 print("Retrying clone after deleting %s" % force_sync, file=sys.stderr)
2186 if force_sync: 2191 if force_sync:
2192 print("Retrying clone after deleting %s" %
2193 self.gitdir, file=sys.stderr)
2187 try: 2194 try:
2188 shutil.rmtree(os.path.realpath(self.gitdir)) 2195 shutil.rmtree(os.path.realpath(self.gitdir))
2189 if self.worktree and os.path.exists( 2196 if self.worktree and os.path.exists(os.path.realpath
2190 os.path.realpath(self.worktree)): 2197 (self.worktree)):
2191 shutil.rmtree(os.path.realpath(self.worktree)) 2198 shutil.rmtree(os.path.realpath(self.worktree))
2192 return self._InitGitDir(mirror_git=mirror_git, force_sync=False) 2199 return self._InitGitDir(mirror_git=mirror_git, force_sync=False)
2193 except: 2200 except:
@@ -2246,7 +2253,7 @@ class Project(object):
2246 name = os.path.basename(stock_hook) 2253 name = os.path.basename(stock_hook)
2247 2254
2248 if name in ('commit-msg',) and not self.remote.review \ 2255 if name in ('commit-msg',) and not self.remote.review \
2249 and not self is self.manifest.manifestProject: 2256 and self is not self.manifest.manifestProject:
2250 # Don't install a Gerrit Code Review hook if this 2257 # Don't install a Gerrit Code Review hook if this
2251 # project does not appear to use it for reviews. 2258 # project does not appear to use it for reviews.
2252 # 2259 #
@@ -2261,7 +2268,8 @@ class Project(object):
2261 if filecmp.cmp(stock_hook, dst, shallow=False): 2268 if filecmp.cmp(stock_hook, dst, shallow=False):
2262 os.remove(dst) 2269 os.remove(dst)
2263 else: 2270 else:
2264 _error("%s: Not replacing %s hook", self.relpath, name) 2271 _warn("%s: Not replacing locally modified %s hook",
2272 self.relpath, name)
2265 continue 2273 continue
2266 try: 2274 try:
2267 os.symlink(os.path.relpath(stock_hook, os.path.dirname(dst)), dst) 2275 os.symlink(os.path.relpath(stock_hook, os.path.dirname(dst)), dst)
@@ -2320,7 +2328,10 @@ class Project(object):
2320 # Fail if the links are pointing to the wrong place 2328 # Fail if the links are pointing to the wrong place
2321 if src != dst: 2329 if src != dst:
2322 raise GitError('--force-sync not enabled; cannot overwrite a local ' 2330 raise GitError('--force-sync not enabled; cannot overwrite a local '
2323 'work tree') 2331 'work tree. If you\'re comfortable with the '
2332 'possibility of losing the work tree\'s git metadata,'
2333 ' use `repo sync --force-sync {0}` to '
2334 'proceed.'.format(self.relpath))
2324 2335
2325 def _ReferenceGitDir(self, gitdir, dotgit, share_refs, copy_all): 2336 def _ReferenceGitDir(self, gitdir, dotgit, share_refs, copy_all):
2326 """Update |dotgit| to reference |gitdir|, using symlinks where possible. 2337 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
@@ -2464,6 +2475,7 @@ class Project(object):
2464 return logs 2475 return logs
2465 2476
2466 class _GitGetByExec(object): 2477 class _GitGetByExec(object):
2478
2467 def __init__(self, project, bare, gitdir): 2479 def __init__(self, project, bare, gitdir):
2468 self._project = project 2480 self._project = project
2469 self._bare = bare 2481 self._bare = bare
@@ -2482,8 +2494,8 @@ class Project(object):
2482 if p.Wait() == 0: 2494 if p.Wait() == 0:
2483 out = p.stdout 2495 out = p.stdout
2484 if out: 2496 if out:
2497 # Backslash is not anomalous
2485 return out[:-1].split('\0') # pylint: disable=W1401 2498 return out[:-1].split('\0') # pylint: disable=W1401
2486 # Backslash is not anomalous
2487 return [] 2499 return []
2488 2500
2489 def DiffZ(self, name, *args): 2501 def DiffZ(self, name, *args):
@@ -2509,6 +2521,7 @@ class Project(object):
2509 break 2521 break
2510 2522
2511 class _Info(object): 2523 class _Info(object):
2524
2512 def __init__(self, path, omode, nmode, oid, nid, state): 2525 def __init__(self, path, omode, nmode, oid, nid, state):
2513 self.path = path 2526 self.path = path
2514 self.src_path = None 2527 self.src_path = None
@@ -2611,10 +2624,8 @@ class Project(object):
2611 line = line[:-1] 2624 line = line[:-1]
2612 r.append(line) 2625 r.append(line)
2613 if p.Wait() != 0: 2626 if p.Wait() != 0:
2614 raise GitError('%s rev-list %s: %s' % ( 2627 raise GitError('%s rev-list %s: %s' %
2615 self._project.name, 2628 (self._project.name, str(args), p.stderr))
2616 str(args),
2617 p.stderr))
2618 return r 2629 return r
2619 2630
2620 def __getattr__(self, name): 2631 def __getattr__(self, name):
@@ -2637,6 +2648,7 @@ class Project(object):
2637 A callable object that will try to call git with the named command. 2648 A callable object that will try to call git with the named command.
2638 """ 2649 """
2639 name = name.replace('_', '-') 2650 name = name.replace('_', '-')
2651
2640 def runner(*args, **kwargs): 2652 def runner(*args, **kwargs):
2641 cmdv = [] 2653 cmdv = []
2642 config = kwargs.pop('config', None) 2654 config = kwargs.pop('config', None)
@@ -2659,10 +2671,8 @@ class Project(object):
2659 capture_stdout=True, 2671 capture_stdout=True,
2660 capture_stderr=True) 2672 capture_stderr=True)
2661 if p.Wait() != 0: 2673 if p.Wait() != 0:
2662 raise GitError('%s %s: %s' % ( 2674 raise GitError('%s %s: %s' %
2663 self._project.name, 2675 (self._project.name, name, p.stderr))
2664 name,
2665 p.stderr))
2666 r = p.stdout 2676 r = p.stdout
2667 try: 2677 try:
2668 r = r.decode('utf-8') 2678 r = r.decode('utf-8')
@@ -2675,14 +2685,19 @@ class Project(object):
2675 2685
2676 2686
2677class _PriorSyncFailedError(Exception): 2687class _PriorSyncFailedError(Exception):
2688
2678 def __str__(self): 2689 def __str__(self):
2679 return 'prior sync failed; rebase still in progress' 2690 return 'prior sync failed; rebase still in progress'
2680 2691
2692
2681class _DirtyError(Exception): 2693class _DirtyError(Exception):
2694
2682 def __str__(self): 2695 def __str__(self):
2683 return 'contains uncommitted changes' 2696 return 'contains uncommitted changes'
2684 2697
2698
2685class _InfoMessage(object): 2699class _InfoMessage(object):
2700
2686 def __init__(self, project, text): 2701 def __init__(self, project, text):
2687 self.project = project 2702 self.project = project
2688 self.text = text 2703 self.text = text
@@ -2691,7 +2706,9 @@ class _InfoMessage(object):
2691 syncbuf.out.info('%s/: %s', self.project.relpath, self.text) 2706 syncbuf.out.info('%s/: %s', self.project.relpath, self.text)
2692 syncbuf.out.nl() 2707 syncbuf.out.nl()
2693 2708
2709
2694class _Failure(object): 2710class _Failure(object):
2711
2695 def __init__(self, project, why): 2712 def __init__(self, project, why):
2696 self.project = project 2713 self.project = project
2697 self.why = why 2714 self.why = why
@@ -2702,7 +2719,9 @@ class _Failure(object):
2702 str(self.why)) 2719 str(self.why))
2703 syncbuf.out.nl() 2720 syncbuf.out.nl()
2704 2721
2722
2705class _Later(object): 2723class _Later(object):
2724
2706 def __init__(self, project, action): 2725 def __init__(self, project, action):
2707 self.project = project 2726 self.project = project
2708 self.action = action 2727 self.action = action
@@ -2719,14 +2738,18 @@ class _Later(object):
2719 out.nl() 2738 out.nl()
2720 return False 2739 return False
2721 2740
2741
2722class _SyncColoring(Coloring): 2742class _SyncColoring(Coloring):
2743
2723 def __init__(self, config): 2744 def __init__(self, config):
2724 Coloring.__init__(self, config, 'reposync') 2745 Coloring.__init__(self, config, 'reposync')
2725 self.project = self.printer('header', attr='bold') 2746 self.project = self.printer('header', attr='bold')
2726 self.info = self.printer('info') 2747 self.info = self.printer('info')
2727 self.fail = self.printer('fail', fg='red') 2748 self.fail = self.printer('fail', fg='red')
2728 2749
2750
2729class SyncBuffer(object): 2751class SyncBuffer(object):
2752
2730 def __init__(self, config, detach_head=False): 2753 def __init__(self, config, detach_head=False):
2731 self._messages = [] 2754 self._messages = []
2732 self._failures = [] 2755 self._failures = []
@@ -2782,8 +2805,10 @@ class SyncBuffer(object):
2782 2805
2783 2806
2784class MetaProject(Project): 2807class MetaProject(Project):
2808
2785 """A special project housed under .repo. 2809 """A special project housed under .repo.
2786 """ 2810 """
2811
2787 def __init__(self, manifest, name, gitdir, worktree): 2812 def __init__(self, manifest, name, gitdir, worktree):
2788 Project.__init__(self, 2813 Project.__init__(self,
2789 manifest=manifest, 2814 manifest=manifest,
@@ -2817,10 +2842,9 @@ class MetaProject(Project):
2817 syncbuf.Finish() 2842 syncbuf.Finish()
2818 2843
2819 return GitCommand(self, 2844 return GitCommand(self,
2820 ['update-ref', '-d', 'refs/heads/default'], 2845 ['update-ref', '-d', 'refs/heads/default'],
2821 capture_stdout=True, 2846 capture_stdout=True,
2822 capture_stderr=True).Wait() == 0 2847 capture_stderr=True).Wait() == 0
2823
2824 2848
2825 @property 2849 @property
2826 def LastFetch(self): 2850 def LastFetch(self):