summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShawn O. Pearce <sop@google.com>2009-04-16 11:21:18 -0700
committerShawn O. Pearce <sop@google.com>2009-04-16 11:21:18 -0700
commit350cde4c4bec5e7b5776cf52d61da600af3efc31 (patch)
tree45c09f4113cf5d7c0d5430360344c5d97d93304e
parent48244781c2cad1565b4b32b4524ff9931a39f848 (diff)
downloadgit-repo-350cde4c4bec5e7b5776cf52d61da600af3efc31.tar.gz
Change repo sync to be more friendly when updating the treev1.6.6
We now try to sync all projects that can be done safely first, before we start rebasing user commits over the upstream. This has the nice effect of making the local tree as close to the upstream as possible before the user has to start resolving merge conflicts, as that extra information in other projects may aid in the conflict resolution. Informational output is buffered and delayed until calculation for all projects has been done, so that the user gets one concise list of notice messages, rather than it interrupting the progress meter. Fast-forward output is now prefixed with the project header, so the user can see which project that update is taking place in, and make some relation of the diffstat back to the project name. Rebase output is now prefixed with the project header, so that if the rebase fails, the user can see which project we were operating on and can try to address the failure themselves. Since rebase sits on a detached HEAD, we now look for an in-progress rebase during sync, so we can alert the user that the given project is in a state we cannot handle. Signed-off-by: Shawn O. Pearce <sop@google.com>
-rw-r--r--color.py3
-rw-r--r--project.py210
-rw-r--r--subcmds/init.py6
-rw-r--r--subcmds/sync.py18
4 files changed, 175 insertions, 62 deletions
diff --git a/color.py b/color.py
index 07d1f6fd..7fa21d23 100644
--- a/color.py
+++ b/color.py
@@ -100,6 +100,9 @@ class Coloring(object):
100 else: 100 else:
101 self._on = False 101 self._on = False
102 102
103 def redirect(self, out):
104 self._out = out
105
103 @property 106 @property
104 def is_on(self): 107 def is_on(self):
105 return self._on 108 return self._on
diff --git a/project.py b/project.py
index 33cb3444..6621f1cb 100644
--- a/project.py
+++ b/project.py
@@ -38,14 +38,6 @@ def _error(fmt, *args):
38 msg = fmt % args 38 msg = fmt % args
39 print >>sys.stderr, 'error: %s' % msg 39 print >>sys.stderr, 'error: %s' % msg
40 40
41def _warn(fmt, *args):
42 msg = fmt % args
43 print >>sys.stderr, 'warn: %s' % msg
44
45def _info(fmt, *args):
46 msg = fmt % args
47 print >>sys.stderr, 'info: %s' % msg
48
49def not_rev(r): 41def not_rev(r):
50 return '^' + r 42 return '^' + r
51 43
@@ -576,13 +568,9 @@ class Project(object):
576 for file in self.copyfiles: 568 for file in self.copyfiles:
577 file._Copy() 569 file._Copy()
578 570
579 def Sync_LocalHalf(self, detach_head=False): 571 def Sync_LocalHalf(self, syncbuf):
580 """Perform only the local IO portion of the sync process. 572 """Perform only the local IO portion of the sync process.
581 Network access is not required. 573 Network access is not required.
582
583 Return:
584 True: the sync was successful
585 False: the sync requires user input
586 """ 574 """
587 self._InitWorkTree() 575 self._InitWorkTree()
588 self.CleanPublishedCache() 576 self.CleanPublishedCache()
@@ -597,19 +585,25 @@ class Project(object):
597 585
598 branch = self.CurrentBranch 586 branch = self.CurrentBranch
599 587
600 if branch is None or detach_head: 588 if branch is None or syncbuf.detach_head:
601 # Currently on a detached HEAD. The user is assumed to 589 # Currently on a detached HEAD. The user is assumed to
602 # not have any local modifications worth worrying about. 590 # not have any local modifications worth worrying about.
603 # 591 #
592 if os.path.exists(os.path.join(self.worktree, '.dotest')) \
593 or os.path.exists(os.path.join(self.worktree, '.git', 'rebase-apply')):
594 syncbuf.fail(self, _PriorSyncFailedError())
595 return
596
604 lost = self._revlist(not_rev(rev), HEAD) 597 lost = self._revlist(not_rev(rev), HEAD)
605 if lost: 598 if lost:
606 _info("[%s] Discarding %d commits", self.name, len(lost)) 599 syncbuf.info(self, "discarding %d commits", len(lost))
607 try: 600 try:
608 self._Checkout(rev, quiet=True) 601 self._Checkout(rev, quiet=True)
609 except GitError: 602 except GitError, e:
610 return False 603 syncbuf.fail(self, e)
604 return
611 self._CopyFiles() 605 self._CopyFiles()
612 return True 606 return
613 607
614 branch = self.GetBranch(branch) 608 branch = self.GetBranch(branch)
615 merge = branch.LocalMerge 609 merge = branch.LocalMerge
@@ -618,16 +612,16 @@ class Project(object):
618 # The current branch has no tracking configuration. 612 # The current branch has no tracking configuration.
619 # Jump off it to a deatched HEAD. 613 # Jump off it to a deatched HEAD.
620 # 614 #
621 _info("[%s] Leaving %s" 615 syncbuf.info(self,
622 " (does not track any upstream)", 616 "leaving %s; does not track upstream",
623 self.name, 617 branch.name)
624 branch.name)
625 try: 618 try:
626 self._Checkout(rev, quiet=True) 619 self._Checkout(rev, quiet=True)
627 except GitError: 620 except GitError, e:
628 return False 621 syncbuf.fail(self, e)
622 return
629 self._CopyFiles() 623 self._CopyFiles()
630 return True 624 return
631 625
632 upstream_gain = self._revlist(not_rev(HEAD), rev) 626 upstream_gain = self._revlist(not_rev(HEAD), rev)
633 pub = self.WasPublished(branch.name) 627 pub = self.WasPublished(branch.name)
@@ -639,25 +633,24 @@ class Project(object):
639 # commits are not yet merged upstream. We do not want 633 # commits are not yet merged upstream. We do not want
640 # to rewrite the published commits so we punt. 634 # to rewrite the published commits so we punt.
641 # 635 #
642 _info("[%s] Branch %s is published," 636 syncbuf.info(self,
643 " but is now %d commits behind.", 637 "branch %s is published but is now %d commits behind",
644 self.name, branch.name, len(upstream_gain)) 638 branch.name,
645 _info("[%s] Consider merging or rebasing the" 639 len(upstream_gain))
646 " unpublished commits.", self.name) 640 syncbuf.info(self, "consider merging or rebasing the unpublished commits")
647 return True 641 return
648 elif upstream_gain: 642 elif upstream_gain:
649 # We can fast-forward safely. 643 # We can fast-forward safely.
650 # 644 #
651 try: 645 def _doff():
652 self._FastForward(rev) 646 self._FastForward(rev)
653 except GitError: 647 self._CopyFiles()
654 return False 648 syncbuf.later1(self, _doff)
655 self._CopyFiles() 649 return
656 return True
657 else: 650 else:
658 # Trivially no changes in the upstream. 651 # Trivially no changes in the upstream.
659 # 652 #
660 return True 653 return
661 654
662 if merge == rev: 655 if merge == rev:
663 try: 656 try:
@@ -672,8 +665,7 @@ class Project(object):
672 # and pray that the old upstream also wasn't in the habit 665 # and pray that the old upstream also wasn't in the habit
673 # of rebasing itself. 666 # of rebasing itself.
674 # 667 #
675 _info("[%s] Manifest switched from %s to %s", 668 syncbuf.info(self, "manifest switched %s...%s", merge, rev)
676 self.name, merge, rev)
677 old_merge = merge 669 old_merge = merge
678 670
679 if rev == old_merge: 671 if rev == old_merge:
@@ -684,19 +676,19 @@ class Project(object):
684 if not upstream_lost and not upstream_gain: 676 if not upstream_lost and not upstream_gain:
685 # Trivially no changes caused by the upstream. 677 # Trivially no changes caused by the upstream.
686 # 678 #
687 return True 679 return
688 680
689 if self.IsDirty(consider_untracked=False): 681 if self.IsDirty(consider_untracked=False):
690 _warn('[%s] commit (or discard) uncommitted changes' 682 syncbuf.fail(self, _DirtyError())
691 ' before sync', self.name) 683 return
692 return False
693 684
694 if upstream_lost: 685 if upstream_lost:
695 # Upstream rebased. Not everything in HEAD 686 # Upstream rebased. Not everything in HEAD
696 # may have been caused by the user. 687 # may have been caused by the user.
697 # 688 #
698 _info("[%s] Discarding %d commits removed from upstream", 689 syncbuf.info(self,
699 self.name, len(upstream_lost)) 690 "discarding %d commits removed from upstream",
691 len(upstream_lost))
700 692
701 branch.remote = rem 693 branch.remote = rem
702 branch.merge = self.revision 694 branch.merge = self.revision
@@ -704,23 +696,22 @@ class Project(object):
704 696
705 my_changes = self._revlist(not_rev(old_merge), HEAD) 697 my_changes = self._revlist(not_rev(old_merge), HEAD)
706 if my_changes: 698 if my_changes:
707 try: 699 def _dorebase():
708 self._Rebase(upstream = old_merge, onto = rev) 700 self._Rebase(upstream = old_merge, onto = rev)
709 except GitError: 701 self._CopyFiles()
710 return False 702 syncbuf.later2(self, _dorebase)
711 elif upstream_lost: 703 elif upstream_lost:
712 try: 704 try:
713 self._ResetHard(rev) 705 self._ResetHard(rev)
714 except GitError: 706 self._CopyFiles()
715 return False 707 except GitError, e:
708 syncbuf.fail(self, e)
709 return
716 else: 710 else:
717 try: 711 def _doff():
718 self._FastForward(rev) 712 self._FastForward(rev)
719 except GitError: 713 self._CopyFiles()
720 return False 714 syncbuf.later1(self, _doff)
721
722 self._CopyFiles()
723 return True
724 715
725 def AddCopyFile(self, src, dest, absdest): 716 def AddCopyFile(self, src, dest, absdest):
726 # dest should already be an absolute path, but src is project relative 717 # dest should already be an absolute path, but src is project relative
@@ -1212,6 +1203,113 @@ class Project(object):
1212 return runner 1203 return runner
1213 1204
1214 1205
1206class _PriorSyncFailedError(Exception):
1207 def __str__(self):
1208 return 'prior sync failed; rebase still in progress'
1209
1210class _DirtyError(Exception):
1211 def __str__(self):
1212 return 'contains uncommitted changes'
1213
1214class _InfoMessage(object):
1215 def __init__(self, project, text):
1216 self.project = project
1217 self.text = text
1218
1219 def Print(self, syncbuf):
1220 syncbuf.out.info('%s/: %s', self.project.relpath, self.text)
1221 syncbuf.out.nl()
1222
1223class _Failure(object):
1224 def __init__(self, project, why):
1225 self.project = project
1226 self.why = why
1227
1228 def Print(self, syncbuf):
1229 syncbuf.out.fail('error: %s/: %s',
1230 self.project.relpath,
1231 str(self.why))
1232 syncbuf.out.nl()
1233
1234class _Later(object):
1235 def __init__(self, project, action):
1236 self.project = project
1237 self.action = action
1238
1239 def Run(self, syncbuf):
1240 out = syncbuf.out
1241 out.project('project %s/', self.project.relpath)
1242 out.nl()
1243 try:
1244 self.action()
1245 out.nl()
1246 return True
1247 except GitError, e:
1248 out.nl()
1249 return False
1250
1251class _SyncColoring(Coloring):
1252 def __init__(self, config):
1253 Coloring.__init__(self, config, 'reposync')
1254 self.project = self.printer('header', attr = 'bold')
1255 self.info = self.printer('info')
1256 self.fail = self.printer('fail', fg='red')
1257
1258class SyncBuffer(object):
1259 def __init__(self, config, detach_head=False):
1260 self._messages = []
1261 self._failures = []
1262 self._later_queue1 = []
1263 self._later_queue2 = []
1264
1265 self.out = _SyncColoring(config)
1266 self.out.redirect(sys.stderr)
1267
1268 self.detach_head = detach_head
1269 self.clean = True
1270
1271 def info(self, project, fmt, *args):
1272 self._messages.append(_InfoMessage(project, fmt % args))
1273
1274 def fail(self, project, err=None):
1275 self._failures.append(_Failure(project, err))
1276 self.clean = False
1277
1278 def later1(self, project, what):
1279 self._later_queue1.append(_Later(project, what))
1280
1281 def later2(self, project, what):
1282 self._later_queue2.append(_Later(project, what))
1283
1284 def Finish(self):
1285 self._PrintMessages()
1286 self._RunLater()
1287 self._PrintMessages()
1288 return self.clean
1289
1290 def _RunLater(self):
1291 for q in ['_later_queue1', '_later_queue2']:
1292 if not self._RunQueue(q):
1293 return
1294
1295 def _RunQueue(self, queue):
1296 for m in getattr(self, queue):
1297 if not m.Run(self):
1298 self.clean = False
1299 return False
1300 setattr(self, queue, [])
1301 return True
1302
1303 def _PrintMessages(self):
1304 for m in self._messages:
1305 m.Print(self)
1306 for m in self._failures:
1307 m.Print(self)
1308
1309 self._messages = []
1310 self._failures = []
1311
1312
1215class MetaProject(Project): 1313class MetaProject(Project):
1216 """A special project housed under .repo. 1314 """A special project housed under .repo.
1217 """ 1315 """
diff --git a/subcmds/init.py b/subcmds/init.py
index a32eaae0..103a78d6 100644
--- a/subcmds/init.py
+++ b/subcmds/init.py
@@ -20,6 +20,7 @@ from color import Coloring
20from command import InteractiveCommand, MirrorSafeCommand 20from command import InteractiveCommand, MirrorSafeCommand
21from error import ManifestParseError 21from error import ManifestParseError
22from remote import Remote 22from remote import Remote
23from project import SyncBuffer
23from git_command import git, MIN_GIT_VERSION 24from git_command import git, MIN_GIT_VERSION
24 25
25class Init(InteractiveCommand, MirrorSafeCommand): 26class Init(InteractiveCommand, MirrorSafeCommand):
@@ -129,7 +130,10 @@ default.xml will be used.
129 print >>sys.stderr, 'fatal: cannot obtain manifest %s' % r.url 130 print >>sys.stderr, 'fatal: cannot obtain manifest %s' % r.url
130 sys.exit(1) 131 sys.exit(1)
131 132
132 m.Sync_LocalHalf() 133 syncbuf = SyncBuffer(m.config)
134 m.Sync_LocalHalf(syncbuf)
135 syncbuf.Finish()
136
133 if is_new or m.CurrentBranch is None: 137 if is_new or m.CurrentBranch is None:
134 if not m.StartBranch('default'): 138 if not m.StartBranch('default'):
135 print >>sys.stderr, 'fatal: cannot create default in manifest' 139 print >>sys.stderr, 'fatal: cannot create default in manifest'
diff --git a/subcmds/sync.py b/subcmds/sync.py
index f6eb2a08..ec5ada21 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -24,6 +24,7 @@ from project import HEAD
24from command import Command, MirrorSafeCommand 24from command import Command, MirrorSafeCommand
25from error import RepoChangedException, GitError 25from error import RepoChangedException, GitError
26from project import R_HEADS 26from project import R_HEADS
27from project import SyncBuffer
27from progress import Progress 28from progress import Progress
28 29
29class Sync(Command, MirrorSafeCommand): 30class Sync(Command, MirrorSafeCommand):
@@ -112,7 +113,9 @@ revision is temporarily needed.
112 return 113 return
113 114
114 if mp.HasChanges: 115 if mp.HasChanges:
115 if not mp.Sync_LocalHalf(): 116 syncbuf = SyncBuffer(mp.config)
117 mp.Sync_LocalHalf(syncbuf)
118 if not syncbuf.Finish():
116 sys.exit(1) 119 sys.exit(1)
117 120
118 self.manifest._Unload() 121 self.manifest._Unload()
@@ -123,14 +126,17 @@ revision is temporarily needed.
123 missing.append(project) 126 missing.append(project)
124 self._Fetch(*missing) 127 self._Fetch(*missing)
125 128
129 syncbuf = SyncBuffer(mp.config,
130 detach_head = opt.detach_head)
126 pm = Progress('Syncing work tree', len(all)) 131 pm = Progress('Syncing work tree', len(all))
127 for project in all: 132 for project in all:
128 pm.update() 133 pm.update()
129 if project.worktree: 134 if project.worktree:
130 if not project.Sync_LocalHalf( 135 project.Sync_LocalHalf(syncbuf)
131 detach_head=opt.detach_head):
132 sys.exit(1)
133 pm.end() 136 pm.end()
137 print >>sys.stderr
138 if not syncbuf.Finish():
139 sys.exit(1)
134 140
135 141
136def _PostRepoUpgrade(manifest): 142def _PostRepoUpgrade(manifest):
@@ -143,7 +149,9 @@ def _PostRepoFetch(rp, no_repo_verify=False, verbose=False):
143 print >>sys.stderr, 'info: A new version of repo is available' 149 print >>sys.stderr, 'info: A new version of repo is available'
144 print >>sys.stderr, '' 150 print >>sys.stderr, ''
145 if no_repo_verify or _VerifyTag(rp): 151 if no_repo_verify or _VerifyTag(rp):
146 if not rp.Sync_LocalHalf(): 152 syncbuf = SyncBuffer(rp.config)
153 rp.Sync_LocalHalf(syncbuf)
154 if not syncbuf.Finish():
147 sys.exit(1) 155 sys.exit(1)
148 print >>sys.stderr, 'info: Restarting repo with latest version' 156 print >>sys.stderr, 'info: Restarting repo with latest version'
149 raise RepoChangedException(['--repo-upgraded']) 157 raise RepoChangedException(['--repo-upgraded'])