diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py index ea97744c4801b3825aa32a5b7a45424ace79a834_bWVyY3VyaWFsL2xvY2FscmVwby5weQ==..c8eda7bbdcab193934f04e43a4ee592c139be028_bWVyY3VyaWFsL2xvY2FscmVwby5weQ== 100644 --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -550,6 +550,9 @@ continue node, label = l.split(" ", 1) label = encoding.tolocal(label.strip()) + if not node in self: + raise ValueError('invalidating branch cache because node '+ + '%s does not exist' % node) partial.setdefault(label, []).append(bin(node)) except KeyboardInterrupt: raise @@ -571,6 +574,10 @@ pass def _updatebranchcache(self, partial, ctxgen): + """Given a branchhead cache, partial, that may have extra nodes or be + missing heads, and a generator of nodes that are at least a superset of + heads missing, this function updates partial to be correct. + """ # collect new branch entries newbranches = {} for c in ctxgen: @@ -574,7 +581,7 @@ # collect new branch entries newbranches = {} for c in ctxgen: - newbranches.setdefault(c.branch(), []).append(c.rev()) + newbranches.setdefault(c.branch(), []).append(c.node()) # if older branchheads are reachable from new ones, they aren't # really branchheads. Note checking parents is insufficient: # 1 (branch a) -> 2 (branch b) -> 3 (branch a) @@ -578,15 +585,35 @@ # if older branchheads are reachable from new ones, they aren't # really branchheads. Note checking parents is insufficient: # 1 (branch a) -> 2 (branch b) -> 3 (branch a) - for branch, newrevs in newbranches.iteritems(): - bheadrevs = [self.changelog.rev(node) for node in - partial.setdefault(branch, [])] - bheadrevs.extend(newrevs) - bheadrevs.sort() - # starting from tip means fewer passes over ancestors - newrevs.sort() - while newrevs: - latest = newrevs.pop() + for branch, newnodes in newbranches.iteritems(): + bheads = partial.setdefault(branch, []) + # Remove candidate heads that no longer are in the repo (e.g., as + # the result of a strip that just happened). Avoid using 'node in + # self' here because that dives down into branchcache code somewhat + # recrusively. + bheadrevs = [self.changelog.rev(node) for node in bheads + if self.changelog.hasnode(node)] + newheadrevs = [self.changelog.rev(node) for node in newnodes + if self.changelog.hasnode(node)] + ctxisnew = bheadrevs and min(newheadrevs) > max(bheadrevs) + # Remove duplicates - nodes that are in newheadrevs and are already + # in bheadrevs. This can happen if you strip a node whose parent + # was already a head (because they're on different branches). + bheadrevs = sorted(set(bheadrevs).union(newheadrevs)) + + # Starting from tip means fewer passes over reachable. If we know + # the new candidates are not ancestors of existing heads, we don't + # have to examine ancestors of existing heads + if ctxisnew: + iterrevs = sorted(newheadrevs) + else: + iterrevs = list(bheadrevs) + + # This loop prunes out two kinds of heads - heads that are + # superceded by a head in newheadrevs, and newheadrevs that are not + # heads because an existing head is their descendant. + while iterrevs: + latest = iterrevs.pop() if latest not in bheadrevs: continue ancestors = set(self.changelog.ancestors([latest], @@ -595,6 +622,17 @@ bheadrevs = [b for b in bheadrevs if b not in ancestors] partial[branch] = [self.changelog.node(rev) for rev in bheadrevs] + # There may be branches that cease to exist when the last commit in the + # branch was stripped. This code filters them out. Note that the + # branch that ceased to exist may not be in newbranches because + # newbranches is the set of candidate heads, which when you strip the + # last commit in a branch will be the parent branch. + for branch in partial: + nodes = [head for head in partial[branch] + if self.changelog.hasnode(head)] + if not nodes: + del partial[branch] + def lookup(self, key): return self[key].node() @@ -857,6 +895,9 @@ else: ui.status(_('working directory now based on ' 'revision %d\n') % parents) + # TODO: if we know which new heads may result from this rollback, pass + # them to destroy(), which will prevent the branchhead cache from being + # invalidated. self.destroyed() return 0 @@ -1303,6 +1344,6 @@ tr.release() lock.release() - def destroyed(self): + def destroyed(self, newheadnodes=None): '''Inform the repository that nodes have been destroyed. Intended for use by strip and rollback, so there's a common @@ -1307,8 +1348,23 @@ '''Inform the repository that nodes have been destroyed. Intended for use by strip and rollback, so there's a common - place for anything that has to be done after destroying history.''' - # XXX it might be nice if we could take the list of destroyed - # nodes, but I don't see an easy way for rollback() to do that + place for anything that has to be done after destroying history. + + If you know the branchheadcache was uptodate before nodes were removed + and you also know the set of candidate new heads that may have resulted + from the destruction, you can set newheadnodes. This will enable the + code to update the branchheads cache, rather than having future code + decide it's invalid and regenrating it from scratch. + ''' + # If we have info, newheadnodes, on how to update the branch cache, do + # it, Otherwise, since nodes were destroyed, the cache is stale and this + # will be caught the next time it is read. + if newheadnodes: + tiprev = len(self) - 1 + ctxgen = (self[node] for node in newheadnodes + if self.changelog.hasnode(node)) + self._updatebranchcache(self._branchcache, ctxgen) + self._writebranchcache(self._branchcache, self.changelog.tip(), + tiprev) # Ensure the persistent tag cache is updated. Doing it now # means that the tag cache only has to worry about destroyed diff --git a/mercurial/repair.py b/mercurial/repair.py index ea97744c4801b3825aa32a5b7a45424ace79a834_bWVyY3VyaWFsL3JlcGFpci5weQ==..c8eda7bbdcab193934f04e43a4ee592c139be028_bWVyY3VyaWFsL3JlcGFpci5weQ== 100644 --- a/mercurial/repair.py +++ b/mercurial/repair.py @@ -56,6 +56,11 @@ return s def strip(ui, repo, nodelist, backup="all", topic='backup'): + # It simplifies the logic around updating the branchheads cache if we only + # have to consider the effect of the stripped revisions and not revisions + # missing because the cache is out-of-date. + repo.updatebranchcache() + cl = repo.changelog # TODO handle undo of merge sets if isinstance(nodelist, str): @@ -63,6 +68,17 @@ striplist = [cl.rev(node) for node in nodelist] striprev = min(striplist) + # Generate set of branches who will have nodes stripped. + striprevs = repo.revs("%ld::", striplist) + stripbranches = set([repo[rev].branch() for rev in striprevs]) + + # Set of potential new heads resulting from the strip. The parents of any + # node removed could be a new head because the node to be removed could have + # been the only child of the parent. + newheadrevs = repo.revs("parents(%ld::) - %ld::", striprevs, striprevs) + newheadnodes = set([cl.node(rev) for rev in newheadrevs]) + newheadbranches = set([repo[rev].branch() for rev in newheadrevs]) + keeppartialbundle = backup == 'strip' # Some revisions with rev > striprev may not be descendants of striprev. @@ -169,4 +185,11 @@ % chgrpfile) raise - repo.destroyed() + if len(stripbranches) == 1 and len(newheadbranches) == 1 \ + and stripbranches == newheadbranches: + repo.destroyed(newheadnodes) + else: + # Multiple branches involved in strip. Will allow branchcache to become + # invalid and later on rebuilt from scratch + repo.destroyed() + diff --git a/tests/test-keyword.t b/tests/test-keyword.t index ea97744c4801b3825aa32a5b7a45424ace79a834_dGVzdHMvdGVzdC1rZXl3b3JkLnQ=..c8eda7bbdcab193934f04e43a4ee592c139be028_dGVzdHMvdGVzdC1rZXl3b3JkLnQ= 100644 --- a/tests/test-keyword.t +++ b/tests/test-keyword.t @@ -502,7 +502,6 @@ $ hg -q commit -d '1 14' -m 'prepare amend' $ hg --debug commit --amend -d '1 15' -m 'amend without changes' | grep keywords - invalidating branch cache (tip differs) overwriting a expanding keywords $ hg -q id a71343332ea9 diff --git a/tests/test-mq-caches.t b/tests/test-mq-caches.t index ea97744c4801b3825aa32a5b7a45424ace79a834_dGVzdHMvdGVzdC1tcS1jYWNoZXMudA==..c8eda7bbdcab193934f04e43a4ee592c139be028_dGVzdHMvdGVzdC1tcS1jYWNoZXMudA== 100644 --- a/tests/test-mq-caches.t +++ b/tests/test-mq-caches.t @@ -36,7 +36,8 @@ $ hg qrefresh -m 'patch 1' $ show_branch_cache tip: 0 - No branch cache + d986d5caac23a7d44a46efc0ddaf5eb9665844cf 0 + d986d5caac23a7d44a46efc0ddaf5eb9665844cf default some regular revisions @@ -75,8 +76,8 @@ $ hg qrefresh -m 'patch 2' $ show_branch_cache 1 tip: 3 - c229711f16da3d7591f89b1b8d963b79bda22714 1 - c229711f16da3d7591f89b1b8d963b79bda22714 bar + 982611f6955f9c48d3365decea203217c945ef0d 2 + 982611f6955f9c48d3365decea203217c945ef0d bar dc25e3827021582e979f600811852e36cbe57341 foo branch foo: 3 branch bar: 2 diff --git a/tests/test-rebase-cache.t b/tests/test-rebase-cache.t index ea97744c4801b3825aa32a5b7a45424ace79a834_dGVzdHMvdGVzdC1yZWJhc2UtY2FjaGUudA==..c8eda7bbdcab193934f04e43a4ee592c139be028_dGVzdHMvdGVzdC1yZWJhc2UtY2FjaGUudA== 100644 --- a/tests/test-rebase-cache.t +++ b/tests/test-rebase-cache.t @@ -2,6 +2,7 @@ > [extensions] > graphlog= > rebase= + > mq= > > [phases] > publish=False @@ -262,4 +263,80 @@ $ hg verify -q +Stripping multiple branches in one go bypasses the fast-case code to +update the branch cache. + + $ hg strip 2 + 0 files updated, 0 files merged, 4 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/a3/.hg/strip-backup/*-backup.hg (glob) + + $ hg tglog + o 3: 'C' branch2 + | + o 2: 'branch2' branch2 + | + | @ 1: 'branch1' branch1 + |/ + o 0: 'A' + + + $ hg branches + branch2 3:e4fdb121d036 + branch1 1:63379ac49655 + default 0:1994f17a630e (inactive) + + $ hg theads + 3: 'C' branch2 + 1: 'branch1' branch1 + 0: 'A' + +Fast path branchcache code should not be invoked if branches stripped is not +the same as branches remaining. + + $ hg init b + $ cd b + + $ hg branch branch1 + marked working directory as branch branch1 + (branches are permanent and global, did you want a bookmark?) + $ hg ci -m 'branch1' + + $ hg branch branch2 + marked working directory as branch branch2 + (branches are permanent and global, did you want a bookmark?) + $ hg ci -m 'branch2' + + $ hg branch -f branch1 + marked working directory as branch branch1 + (branches are permanent and global, did you want a bookmark?) + + $ echo a > A + $ hg ci -Am A + adding A + created new head + + $ hg tglog + @ 2: 'A' branch1 + | + o 1: 'branch2' branch2 + | + o 0: 'branch1' branch1 + + + $ hg theads + 2: 'A' branch1 + 1: 'branch2' branch2 + + $ hg strip 2 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/a3/b/.hg/strip-backup/*-backup.hg (glob) + + $ hg theads + 1: 'branch2' branch2 + 0: 'branch1' branch1 + + +Make sure requesting to strip a revision already stripped does not confuse things. +Try both orders. + $ cd .. @@ -265,1 +342,46 @@ $ cd .. + + $ hg init c + $ cd c + + $ echo a > a + $ hg ci -Am A + adding a + $ echo b > b + $ hg ci -Am B + adding b + $ echo c > c + $ hg ci -Am C + adding c + $ echo d > d + $ hg ci -Am D + adding d + $ echo e > e + $ hg ci -Am E + adding e + + $ hg tglog + @ 4: 'E' + | + o 3: 'D' + | + o 2: 'C' + | + o 1: 'B' + | + o 0: 'A' + + + $ hg strip 3 4 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/a3/c/.hg/strip-backup/*-backup.hg (glob) + + $ hg theads + 2: 'C' + + $ hg strip 2 1 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/a3/c/.hg/strip-backup/*-backup.hg (glob) + + $ hg theads + 0: 'A' diff --git a/tests/test-rebase-collapse.t b/tests/test-rebase-collapse.t index ea97744c4801b3825aa32a5b7a45424ace79a834_dGVzdHMvdGVzdC1yZWJhc2UtY29sbGFwc2UudA==..c8eda7bbdcab193934f04e43a4ee592c139be028_dGVzdHMvdGVzdC1yZWJhc2UtY29sbGFwc2UudA== 100644 --- a/tests/test-rebase-collapse.t +++ b/tests/test-rebase-collapse.t @@ -2,6 +2,7 @@ > [extensions] > graphlog= > rebase= + > mq= > > [phases] > publish=False @@ -259,6 +260,44 @@ $ cd .. + + +Test that branchheads cache is updated correctly when doing a strip in which +the parent of the ancestor node to be stripped does not become a head and +also, the parent of a node that is a child of the node stripped becomes a head +(node 3). + + $ hg clone -q -u . b b2 + $ cd b2 + + $ hg heads --template="{rev}:{node} {branch}\n" + 7:c65502d4178782309ce0574c5ae6ee9485a9bafa default + 6:c772a8b2dc17629cec88a19d09c926c4814b12c7 default + + $ cat $TESTTMP/b2/.hg/cache/branchheads + c65502d4178782309ce0574c5ae6ee9485a9bafa 7 + c772a8b2dc17629cec88a19d09c926c4814b12c7 default + c65502d4178782309ce0574c5ae6ee9485a9bafa default + + $ hg strip 4 + saved backup bundle to $TESTTMP/b2/.hg/strip-backup/8a5212ebc852-backup.hg + + $ cat $TESTTMP/b2/.hg/cache/branchheads + c65502d4178782309ce0574c5ae6ee9485a9bafa 4 + 2870ad076e541e714f3c2bc32826b5c6a6e5b040 default + c65502d4178782309ce0574c5ae6ee9485a9bafa default + + $ hg heads --template="{rev}:{node} {branch}\n" + 4:c65502d4178782309ce0574c5ae6ee9485a9bafa default + 3:2870ad076e541e714f3c2bc32826b5c6a6e5b040 default + + $ cd .. + + + + + + Create repo c: $ hg init c @@ -628,3 +667,56 @@ b b $ cd .. + + +Test stripping a revision with another child + + $ hg init f + $ cd f + + $ echo A > A + $ hg ci -Am A + adding A + $ echo B > B + $ hg ci -Am B + adding B + + $ hg up -q 0 + + $ echo C > C + $ hg ci -Am C + adding C + created new head + + $ hg tglog + @ 2: 'C' + | + | o 1: 'B' + |/ + o 0: 'A' + + + + $ hg heads --template="{rev}:{node} {branch}: {desc}\n" + 2:c5cefa58fd557f84b72b87f970135984337acbc5 default: C + 1:27547f69f25460a52fff66ad004e58da7ad3fb56 default: B + + $ hg strip 2 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/f/.hg/strip-backup/*-backup.hg (glob) + + $ hg tglog + o 1: 'B' + | + @ 0: 'A' + + + + $ hg heads --template="{rev}:{node} {branch}: {desc}\n" + 1:27547f69f25460a52fff66ad004e58da7ad3fb56 default: B + + $ cd .. + + + +