diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -1500,110 +1500,106 @@ class localrepository(repo.repository): remote_heads = remote.heads() inc = self.findincoming(remote, common, remote_heads, force=force) + cl = self.changelog update, updated_heads = self.findoutgoing(remote, common, remote_heads) - msng_cl, bases, heads = self.changelog.nodesbetween(update, revs) - - def checkbranch(lheads, rheads, lheadcnt, branchname=None): - ''' - check whether there are more local heads than remote heads on - a specific branch. - - lheads: local branch heads - rheads: remote branch heads - lheadcnt: total number of local branch heads - ''' - - warn = 0 - - if len(lheads) > len(rheads): - warn = 1 - else: - # add local heads involved in the push - updatelheads = [self.changelog.heads(x, lheads) - for x in update] - newheads = set(sum(updatelheads, [])) & set(lheads) - - if not newheads: - return True - - # add heads we don't have or that are not involved in the push - for r in rheads: - if r in self.changelog.nodemap: - desc = self.changelog.heads(r, heads) - l = [h for h in heads if h in desc] - if not l: - newheads.add(r) - else: - newheads.add(r) - if len(newheads) > len(rheads): - warn = 1 - - if warn: - if branchname is not None: - msg = _("abort: push creates new remote heads" - " on branch '%s'!\n") % branchname - else: - msg = _("abort: push creates new remote heads!\n") - self.ui.warn(msg) - if lheadcnt > len(rheads): - self.ui.status(_("(did you forget to merge?" - " use push -f to force)\n")) - else: - self.ui.status(_("(you should pull and merge or" - " use push -f to force)\n")) - return False - return True + outg, bases, heads = cl.nodesbetween(update, revs) if not bases: self.ui.status(_("no changes found\n")) return None, 1 - elif not force: - # Check for each named branch if we're creating new remote heads. - # To be a remote head after push, node must be either: - # - unknown locally - # - a local outgoing head descended from update - # - a remote head that's known locally and not - # ancestral to an outgoing head - # - # New named branches cannot be created without --force. + + if not force and remote_heads != [nullid]: + + def fail_multiple_heads(unsynced, branch=None): + if branch: + msg = _("abort: push creates new remote heads" + " on branch '%s'!\n") % branch + else: + msg = _("abort: push creates new remote heads!\n") + self.ui.warn(msg) + if unsynced: + self.ui.status(_("(you should pull and merge or" + " use push -f to force)\n")) + else: + self.ui.status(_("(did you forget to merge?" + " use push -f to force)\n")) + return None, 0 - if remote_heads != [nullid]: - if remote.capable('branchmap'): - remotebrheads = remote.branchmap() + if remote.capable('branchmap'): + # Check for each named branch if we're creating new remote heads. + # To be a remote head after push, node must be either: + # - unknown locally + # - a local outgoing head descended from update + # - a remote head that's known locally and not + # ancestral to an outgoing head + # + # New named branches cannot be created without --force. + + # 1. Create set of branches involved in the push. + branches = set(self[n].branch() for n in outg) + + # 2. Check for new branches on the remote. + remotemap = remote.branchmap() + newbranches = branches - set(remotemap) + if newbranches: # new branch requires --force + branchnames = ', '.join("%s" % b for b in newbranches) + self.ui.warn(_("abort: push creates " + "new remote branches: %s!\n") + % branchnames) + self.ui.status(_("(use 'hg push -f' to force)\n")) + return None, 0 - lbrmap = self.branchmap() - localbrheads = {} - if not revs: - for br, hds in lbrmap.iteritems(): - localbrheads[br] = (len(hds), hds) - else: - ctxgen = (self[n] for n in msng_cl) - self._updatebranchcache(localbrheads, ctxgen) - for br, hds in localbrheads.iteritems(): - localbrheads[br] = (len(lbrmap[br]), hds) + # 3. Construct the initial oldmap and newmap dicts. + # They contain information about the remote heads before and + # after the push, respectively. + # Heads not found locally are not included in either dict, + # since they won't be affected by the push. + # unsynced contains all branches with incoming changesets. + oldmap = {} + newmap = {} + unsynced = set() + for branch in branches: + remoteheads = remotemap[branch] + prunedheads = [h for h in remoteheads if h in cl.nodemap] + oldmap[branch] = prunedheads + newmap[branch] = list(prunedheads) + if len(remoteheads) > len(prunedheads): + unsynced.add(branch) + + # 4. Update newmap with outgoing changes. + # This will possibly add new heads and remove existing ones. + ctxgen = (self[n] for n in outg) + self._updatebranchcache(newmap, ctxgen) - newbranches = list(set(localbrheads) - set(remotebrheads)) - if newbranches: # new branch requires --force - branchnames = ', '.join("%s" % b for b in newbranches) - self.ui.warn(_("abort: push creates " - "new remote branches: %s!\n") - % branchnames) - # propose 'push -b .' in the msg too? - self.ui.status(_("(use 'hg push -f' to force)\n")) - return None, 0 - for branch, x in localbrheads.iteritems(): - if branch in remotebrheads: - headcnt, lheads = x - rheads = remotebrheads[branch] - if not checkbranch(lheads, rheads, headcnt, branch): - return None, 0 - else: - if not checkbranch(heads, remote_heads, len(heads)): - return None, 0 + # 5. Check for new heads. + # If there are more heads after the push than before, a suitable + # warning, depending on unsynced status, is displayed. + for branch in branches: + if len(newmap[branch]) > len(oldmap[branch]): + return fail_multiple_heads(branch in unsynced, branch) + + # 6. Check for unsynced changes on involved branches. + if unsynced: + self.ui.warn(_("note: unsynced remote changes!\n")) - if inc: - self.ui.warn(_("note: unsynced remote changes!\n")) - + else: + # Old servers: Check for new topological heads. + # Code based on _updatebranchcache. + newheads = set(h for h in remote_heads if h in cl.nodemap) + oldheadcnt = len(newheads) + newheads.update(outg) + if len(newheads) > 1: + for latest in reversed(outg): + if latest not in newheads: + continue + minhrev = min(cl.rev(h) for h in newheads) + reachable = cl.reachable(latest, cl.node(minhrev)) + reachable.remove(latest) + newheads.difference_update(reachable) + if len(newheads) > oldheadcnt: + return fail_multiple_heads(inc) + if inc: + self.ui.warn(_("note: unsynced remote changes!\n")) if revs is None: # use the fast path, no race possible on push diff --git a/tests/test-push-warn b/tests/test-push-warn --- a/tests/test-push-warn +++ b/tests/test-push-warn @@ -298,6 +298,8 @@ hg glog -R inner --template "{rev}: {bra echo %% outgoing hg out inner --template "{rev}: {branches} {desc}\n" hg push inner +hg push inner -r4 -r5 +hg in inner cd .. exit 0 diff --git a/tests/test-push-warn.out b/tests/test-push-warn.out --- a/tests/test-push-warn.out +++ b/tests/test-push-warn.out @@ -211,7 +211,6 @@ searching for changes 2: A a2 pushing to inner searching for changes -note: unsynced remote changes! adding changesets adding manifests adding file changes @@ -253,7 +252,6 @@ searching for changes 3: A a2 pushing to inner searching for changes -note: unsynced remote changes! adding changesets adding manifests adding file changes @@ -303,3 +301,10 @@ pushing to inner searching for changes abort: push creates new remote heads on branch 'A'! (did you forget to merge? use push -f to force) +pushing to inner +searching for changes +abort: push creates new remote heads on branch 'A'! +(did you forget to merge? use push -f to force) +comparing with inner +searching for changes +no changes found