# HG changeset patch # User Pierre-Yves David # Date 1499009318 -7200 # Node ID 167567795f6c4b8340f9b10439581ff042b797a5 # Parent 26ec0839865b1116417b0781a9063cf9ff453b71# Parent da0d3d4171cbdda1b94f7190477c5fae55be727c compat: merge with mercurial-4.0 diff -r da0d3d4171cb -r 167567795f6c .hgtags --- a/.hgtags Fri Jun 16 19:48:10 2017 +0200 +++ b/.hgtags Sun Jul 02 17:28:38 2017 +0200 @@ -53,3 +53,4 @@ 0af99106b0754426913b5c82fb52dc70d4d299f6 6.2.1 6da4ca7b3e4fc214a363a5cf723554f114b7f51e 6.3.0 e358c0263e4629746a7c925429c951f67d4b02b1 6.3.1 +e60248f26f923f4460682252f7863ff86f7b86b0 6.4.0 diff -r da0d3d4171cb -r 167567795f6c README --- a/README Fri Jun 16 19:48:10 2017 +0200 +++ b/README Sun Jul 02 17:28:38 2017 +0200 @@ -121,18 +121,36 @@ Changelog ========= -6.4.0 - inprogress ------------------- +6.5.0 - in progress +------------------- + + - obslog: gain a --patch flag to display changes introduced by the evolution + (Currently limited to in simple case only) + - stack: also show the unstable status for the current changeset (issue5553) + - stack: properly abort when and unknown topic is requested, + - stack: add basic and raw support for named branches + - topic: changing topic on revs no longer adds extra instability (issue5441) + - topic: topics: rename '--change' flag to '--rev' flag, + - topic: multiple large performance improvements, + - topic: various small output improvement, + +6.4.1 - in progress +------------------- + + - evolve: fix branch preservation for merge + - obsfate: improve support for advanced template reformating + - split: preserve author of the splitted changeset, + +6.4.0 -- 2017-06-16 +------------------- + + - template: signifiant improvement to the '{obsfate}' template (now 4.2+ only) + - template: fix 'successors' and 'precursors' template to expose hex-node - effect flag: the experiment is now active by default, (see 'hg help -e evolve' to opt out) + - effect flag: fix a small bug related to hidden changeset, - obscache: reduce impact on large repository - - template: signifiant improvement to the '{obsfate}' template (now 4.2+ only) - -6.3.2 - in progress -------------------- - - - effect flag: fix a small bug related to hidden changeset, - obshashrange: install a '.max-revs' option see extension help for details 6.3.1 -- 2017-06-01 diff -r da0d3d4171cb -r 167567795f6c debian/changelog --- a/debian/changelog Fri Jun 16 19:48:10 2017 +0200 +++ b/debian/changelog Sun Jul 02 17:28:38 2017 +0200 @@ -1,3 +1,9 @@ +mercurial-evolve (6.4.0-1) unstable; urgency=medium + + * new upstream release + + -- Pierre-Yves David Fri, 16 Jun 2017 20:14:13 +0200 + mercurial-evolve (6.3.1-1) unstable; urgency=medium * new upstream release diff -r da0d3d4171cb -r 167567795f6c hgext3rd/evolve/__init__.py --- a/hgext3rd/evolve/__init__.py Fri Jun 16 19:48:10 2017 +0200 +++ b/hgext3rd/evolve/__init__.py Sun Jul 02 17:28:38 2017 +0200 @@ -20,7 +20,7 @@ this extensions contains significant additions recommended to any user of changeset evolution. -With the extensions various evolution events will display warning (new unstable +With the extension various evolution events will display warning (new unstable changesets, obsolete working copy parent, improved error when accessing hidden revision, etc). @@ -77,14 +77,14 @@ # (recommended 'yes' for server (default)) obshashrange.warm-cache = no -It is recommended to enable the blackbox extension. It gather useful data about -the experiment. It is shipped with Mercurial so no extra install are needed. +It is recommended to enable the blackbox extension. It gathers useful data about +the experiment. It is shipped with Mercurial so no extra install is needed:: [extensions] blackbox = Finally some extra options are available to help tame the experimental -implementation of some of the algorithms: +implementation of some of the algorithms:: [experimental] # restrict cache size to reduce memory consumption @@ -99,19 +99,45 @@ Effect Flag Experiment ====================== -We are experimenting with a way to register what changed between a precursor -and its successors (content, description, parent, etc...). For example, having -this information is helpful to show what changed between an obsolete changeset -and its tipmost successors. This experiment is active by default - -The following config control the experiment:: +Evolve also records what changed between two evolutions of a changeset. For +example, having this information is helpful to understand what changed between +an obsolete changeset and its tipmost successors. + +Evolve currently records: + + - Meta changes, user, date + - Tree movement, branch and parent, did the changeset moved? + - Description, was the commit description edited + - Diff, was there apart from potential diff change due to rebase a change in the diff? + +These flags are lightweight and can be combined, so it's easy to see if 4 +evolutions of the same changeset has just updated the description or if the +content changed and you need to review again the diff. + +The effect flag recording is enabled by default in Evolve 6.4.0 so you have +nothing to do to enjoy it. Now every new evolution that you create will have +the effect flag attached. + +The following config control the effect flag recording:: [experimental] - # deactivate the registration of effect flags in obs markers - evolution.effect-flags = false - -The effect flags are shown in the obglog command output without particular -configuration if you want to inspect them. + # uncomment to deactivate the registration of effect flags in obs markers + # evolution.effect-flags = false + +You can display the effect flags with the command obslog, so if you have a +changeset and you update only the message, you will see: + + $ hg commit -m "WIP + $ hg commit -m "A better commit message!" + $ hg obslog . + @ 8e9045855628 (3133) A better commit message! + | + x 7863a5bb5763 (3132) WIP + rewritten(description) by Boris Feld (Fri Jun 02 12:00:24 2017 +0200) as 8e9045855628 + +Servers does not need to activate the effect flag recording. Effect flags that +you create will not cause interference with other clients or servers without +the effect flag recording. Templates ========= @@ -121,14 +147,14 @@ - precursors, for each obsolete changeset show the closest visible precursors. - - successors, for each obsolete changeset show the closets visible + - successors, for each obsolete changeset show the closests visible successors. It is useful when your working directory is obsolete to see - what are its successors. This informations can also be retrieved with the + what are its successors. This information can also be retrieved with the obslog command and the --all option. - obsfate, for each obsolete changeset display a line summarizing what changed between the changeset and its successors. Dependending on the verbosity level (-q and -v) it display the changeset successors, the users - that created the obsmarkers and the date range of theses changes. + that created the obsmarkers and the date range of these changes. The template itself is not complex, the data are basically a list of successortset. Each successorset is a dict with these fields: @@ -410,10 +436,10 @@ 'evolve') if ui.config('alias', 'grab', None) is None: if os.name == 'nt': - ui.setconfig('alias', 'grab', - "! " + util.hgexecutable() + hgexe = ('"%s"' % util.hgexecutable()) + ui.setconfig('alias', 'grab', "! " + hgexe + " rebase --dest . --rev $@ && " - + util.hgexecutable() + " up tip", + + hgexe + " up tip", 'evolve') else: ui.setconfig('alias', 'grab', @@ -902,7 +928,7 @@ if not ctx.obsolete(): continue - successors = obsolete.successorssets(repo, ctx.node(), cache) + successors = compat.successorssets(repo, ctx.node(), cache) # We can't make any assumptions about how to update the hash if the # cset in question was split or diverged. @@ -1175,14 +1201,14 @@ return p.rev() obs = repo[p] ui = repo.ui - newer = obsolete.successorssets(repo, obs.node()) + newer = compat.successorssets(repo, obs.node()) # search of a parent which is not killed while not newer: ui.debug("stabilize target %s is plain dead," " trying to stabilize on its parent\n" % obs) obs = obs.parents()[0] - newer = obsolete.successorssets(repo, obs.node()) + newer = compat.successorssets(repo, obs.node()) if len(newer) > 1 or len(newer[0]) > 1: raise MultipleSuccessorsError(newer) @@ -1302,11 +1328,11 @@ """Compute sets of commits divergent with a given one""" cache = {} base = {} - for n in obsolete.allprecursors(repo.obsstore, [ctx.node()]): + for n in compat.allprecursors(repo.obsstore, [ctx.node()]): if n == ctx.node(): # a node can't be a base for divergence with itself continue - nsuccsets = obsolete.successorssets(repo, n, cache) + nsuccsets = compat.successorssets(repo, n, cache) for nsuccset in nsuccsets: if ctx.node() in nsuccset: # we are only interested in *other* successor sets @@ -1610,7 +1636,7 @@ tovisit = list(parents(rev)) while tovisit: r = tovisit.pop() - succsets = obsolete.successorssets(repo, tonode(r)) + succsets = compat.successorssets(repo, tonode(r)) if not succsets: tovisit.extend(parents(r)) else: @@ -1658,9 +1684,11 @@ progresscb=None): """Stabilize an unstable changeset""" pctx = orig.p1() + keepbranch = orig.p1().branch() != orig.branch() if len(orig.parents()) == 2: if not pctx.obsolete(): pctx = orig.p2() # second parent is obsolete ? + keepbranch = orig.p2().branch() != orig.branch() elif orig.p2().obsolete(): hint = _("Redo the merge (%s) and use `hg prune " "--succ ` to obsolete the old one") % orig.hex()[:12] @@ -1673,14 +1701,14 @@ ui.warn(_("cannot solve instability of %s, skipping\n") % orig) return False obs = pctx - newer = obsolete.successorssets(repo, obs.node()) + newer = compat.successorssets(repo, obs.node()) # search of a parent which is not killed while not newer or newer == [()]: ui.debug("stabilize target %s is plain dead," " trying to stabilize on its parent\n" % obs) obs = obs.parents()[0] - newer = obsolete.successorssets(repo, obs.node()) + newer = compat.successorssets(repo, obs.node()) if len(newer) > 1: msg = _("skipping %s: divergent rewriting. can't choose " "destination\n") % obs @@ -1718,7 +1746,6 @@ repo.ui.note(todo) if progresscb: progresscb() - keepbranch = orig.p1().branch() != orig.branch() try: relocate(repo, orig, target, pctx, keepbranch) except MergeFailure: @@ -1959,7 +1986,7 @@ """ repo = ctx._repo.unfiltered() for base in repo.set('reverse(allprecursors(%d))', ctx): - newer = obsolete.successorssets(ctx._repo, base.node()) + newer = compat.successorssets(ctx._repo, base.node()) # drop filter and solution including the original ctx newer = [n for n in newer if n and ctx.node() not in n] if newer: @@ -2637,6 +2664,12 @@ finally: lockmod.release(tr, lock, wlock) +def presplitupdate(repo, ui, prev, ctx): + """prepare the working directory for a split (for topic hooking) + """ + hg.update(repo, prev) + commands.revert(ui, repo, rev=ctx.rev(), all=True) + @eh.command( '^split', [('r', 'rev', [], _("revision to split")), @@ -2682,9 +2715,9 @@ if bookactive is not None: repo.ui.status(_("(leaving bookmark %s)\n") % repo._activebookmark) bookmarksmod.deactivate(repo) - hg.update(repo, prev) - - commands.revert(ui, repo, rev=r, all=True) + + # Prepare the working directory + presplitupdate(repo, ui, prev, ctx) def haschanges(): modified, added, removed, deleted = repo.status()[:4] @@ -2694,6 +2727,7 @@ msg += ctx.description() opts['message'] = msg opts['edit'] = True + opts['user'] = ctx.user() while haschanges(): pats = () cmdutil.dorecord(ui, repo, commands.commit, 'commit', False, @@ -2794,7 +2828,7 @@ if not (duplicate or allowdivergence): # The user hasn't yet decided what to do with the revived # cset, let's ask - sset = obsolete.successorssets(repo, ctx.node()) + sset = compat.successorssets(repo, ctx.node()) nodivergencerisk = (len(sset) == 0 or (len(sset) == 1 and len(sset[0]) == 1 and diff -r da0d3d4171cb -r 167567795f6c hgext3rd/evolve/compat.py --- a/hgext3rd/evolve/compat.py Fri Jun 16 19:48:10 2017 +0200 +++ b/hgext3rd/evolve/compat.py Sun Jul 02 17:28:38 2017 +0200 @@ -11,6 +11,12 @@ obsolete ) +try: + from mercurial import obsutil + obsutil.closestpredecessors +except ImportError: + obsutil = None + from . import ( exthelper, ) @@ -55,3 +61,17 @@ pendingnodes -= seennodes seennodes |= pendingnodes return seenmarkers + +# successors set move from mercurial.obsolete to mercurial.obsutil in 4.3 +def successorssets(*args, **kwargs): + func = getattr(obsutil, 'successorssets', None) + if func is None: + func = obsolete.successorssets + return func(*args, **kwargs) + +# allprecursors set move from mercurial.obsolete to mercurial.obsutil in 4.3 +def allprecursors(*args, **kwargs): + func = getattr(obsutil, 'allprecursors', None) + if func is None: + func = obsolete.allprecursors + return func(*args, **kwargs) diff -r da0d3d4171cb -r 167567795f6c hgext3rd/evolve/metadata.py --- a/hgext3rd/evolve/metadata.py Fri Jun 16 19:48:10 2017 +0200 +++ b/hgext3rd/evolve/metadata.py Sun Jul 02 17:28:38 2017 +0200 @@ -5,7 +5,7 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. -__version__ = '6.4.0.dev' +__version__ = '6.5.0.dev' testedwith = '3.8.4 3.9.2 4.0.2 4.1.2 4.2' minimumhgversion = '3.8' buglink = 'https://bz.mercurial-scm.org/' diff -r da0d3d4171cb -r 167567795f6c hgext3rd/evolve/obshistory.py --- a/hgext3rd/evolve/obshistory.py Fri Jun 16 19:48:10 2017 +0200 +++ b/hgext3rd/evolve/obshistory.py Sun Jul 02 17:28:38 2017 +0200 @@ -14,6 +14,8 @@ commands, error, graphmod, + mdiff, + patch, obsolete, node as nodemod, scmutil, @@ -22,6 +24,7 @@ from mercurial.i18n import _ from . import ( + compat, exthelper, ) @@ -31,7 +34,8 @@ 'obslog|olog', [('G', 'graph', True, _("show the revision DAG")), ('r', 'rev', [], _('show the specified revision or revset'), _('REV')), - ('a', 'all', False, _('show all related changesets, not only precursors')) + ('a', 'all', False, _('show all related changesets, not only precursors')), + ('p', 'patch', False, _('show the patch between two obs versions')) ] + commands.formatteropts, _('hg olog [OPTION]... [REV]')) def cmdobshistory(ui, repo, *revs, **opts): @@ -51,8 +55,8 @@ the obsolescence operation (rewritten or pruned) in addition of the user and date of the operation. - The output is a graph by default but can deactivated with the option '--no- - graph'. + The output is a graph by default but can deactivated with the option + '--no-graph'. 'o' is a changeset, '@' is a working directory parent, 'x' is obsolete, and '+' represents a fork where the changeset from the lines below is a @@ -72,7 +76,7 @@ fm = ui.formatter('debugobshistory', opts) revs.reverse() - _debugobshistorysingle(fm, repo, revs) + _debugobshistoryrevs(fm, repo, revs, opts) fm.end() @@ -91,15 +95,17 @@ fm = self.ui.formatter('debugobshistory', props) _debugobshistorydisplaynode(fm, self.repo, changenode) + # Succs markers succs = self.repo.obsstore.successors.get(changenode, ()) + succs = sorted(succs) markerfm = fm.nested("debugobshistory.markers") - for successor in sorted(succs): - _debugobshistorydisplaymarker(markerfm, self.repo, successor) + for successor in succs: + _debugobshistorydisplaymarker(markerfm, successor, + ctx.node(), self.repo, self.diffopts) markerfm.end() markerfm.plain('\n') - self.hunk[ctx.node()] = self.ui.popbuffer() else: ### graph output is buffered only @@ -112,6 +118,71 @@ ''' pass +def patchavailable(node, repo, marker): + if node not in repo: + return False, "context is not local" + + successors = marker[1] + + if len(successors) == 0: + return False, "no successors" + elif len(successors) > 1: + return False, "too many successors (%d)" % len(successors) + + succ = successors[0] + + if succ not in repo: + return False, "succ is unknown locally" + + # Check that both node and succ have the same parents + nodep1, nodep2 = repo[node].p1(), repo[node].p2() + succp1, succp2 = repo[succ].p1(), repo[succ].p2() + + if nodep1 != succp1 or nodep2 != succp2: + return False, "changesets rebased" + + return True, succ + +def _indent(content, indent=4): + extra = ' ' * indent + return "".join(extra + line for line in content.splitlines(True)) + +def getmarkercontentpatch(repo, node, succ): + # Todo get the ops from the cmd + diffopts = patch.diffallopts(repo.ui, {}) + matchfn = scmutil.matchall(repo) + + repo.ui.pushbuffer() + cmdutil.diffordiffstat(repo.ui, repo, diffopts, node, succ, + match=matchfn, stat=False) + buffer = repo.ui.popbuffer() + + return _indent(buffer) + +def getmarkerdescriptionpatch(repo, base, succ): + basectx = repo[base] + succctx = repo[succ] + # description are stored without final new line, + # add one to avoid ugly diff + basedesc = basectx.description() + '\n' + succdesc = succctx.description() + '\n' + + # fake file name + basename = "%s-changeset-description" % basectx + succname = "%s-changeset-description" % succctx + + d = mdiff.unidiff(basedesc, '', succdesc, '', basename, succname) + # mercurial 4.1 and before return the patch directly + if not isinstance(d, tuple): + patch = d + else: + uheaders, hunks = d + + # Copied from patch.diff + text = ''.join(sum((list(hlines) for hrange, hlines in hunks), [])) + patch = "\n".join(uheaders + [text]) + return _indent(patch) + class missingchangectx(object): ''' a minimal object mimicking changectx for change contexts references by obs markers but not available locally ''' @@ -275,17 +346,22 @@ return sorted(seen), nodesucc, nodeprec def _debugobshistorygraph(ui, repo, revs, opts): - displayer = obsmarker_printer(ui, repo.unfiltered(), None, opts, buffered=True) + matchfn = None + if opts.get('patch'): + matchfn = scmutil.matchall(repo) + + displayer = obsmarker_printer(ui, repo.unfiltered(), matchfn, opts, buffered=True) edges = graphmod.asciiedges walker = _obshistorywalker(repo.unfiltered(), revs, opts.get('all', False)) cmdutil.displaygraph(ui, repo, walker, displayer, edges) -def _debugobshistorysingle(fm, repo, revs): - """ Display the obsolescence history for a single revision +def _debugobshistoryrevs(fm, repo, revs, opts): + """ Display the obsolescence history for revset """ precursors = repo.obsstore.precursors successors = repo.obsstore.successors nodec = repo.changelog.node + unfi = repo.unfiltered() nodes = [nodec(r) for r in revs] seen = set(nodes) @@ -293,13 +369,13 @@ while nodes: ctxnode = nodes.pop() - _debugobshistorydisplaynode(fm, repo, ctxnode) + _debugobshistorydisplaynode(fm, unfi, ctxnode) succs = successors.get(ctxnode, ()) markerfm = fm.nested("debugobshistory.markers") for successor in sorted(succs): - _debugobshistorydisplaymarker(markerfm, repo, successor) + _debugobshistorydisplaymarker(markerfm, successor, ctxnode, repo, opts) markerfm.end() precs = precursors.get(ctxnode, ()) @@ -310,8 +386,8 @@ nodes.append(p[0]) def _debugobshistorydisplaynode(fm, repo, node): - if node in repo.unfiltered(): - _debugobshistorydisplayctx(fm, repo.unfiltered()[node]) + if node in repo: + _debugobshistorydisplayctx(fm, repo[node]) else: _debugobshistorydisplaymissingctx(fm, node) @@ -338,7 +414,7 @@ label="evolve.node evolve.missing_change_ctx") fm.plain('\n') -def _debugobshistorydisplaymarker(fm, repo, marker): +def _debugobshistorydisplaymarker(fm, marker, node, repo, opts): succnodes = marker[1] date = marker[4] metadata = dict(marker[3]) @@ -401,6 +477,30 @@ fm.write('debugobshistory.succnodes', '%s', nodes, label="evolve.node") + # Patch display + if opts.get('patch'): + _patchavailable = patchavailable(node, repo, marker) + + if _patchavailable[0] is True: + succ = _patchavailable[1] + + # Description patch + descriptionpatch = getmarkerdescriptionpatch(repo, node, succ) + if descriptionpatch: + fm.plain("\n") + fm.plain(descriptionpatch) + + # Content patch + contentpatch = getmarkercontentpatch(repo, node, succ) + if contentpatch: + fm.plain("\n") + fm.plain(contentpatch) + else: + patch = " (No patch available yet, %s)" % _patchavailable[1] + fm.plain("\n") + # TODO: should be in json too + fm.plain(patch) + fm.plain("\n") # logic around storing and using effect flags @@ -586,7 +686,7 @@ or has diverged """ if successorssets is None: - successorssets = obsolete.successorssets(repo, revnode) + successorssets = compat.successorssets(repo, revnode) fate = _getobsfate(successorssets) diff -r da0d3d4171cb -r 167567795f6c hgext3rd/evolve/templatekw.py --- a/hgext3rd/evolve/templatekw.py Fri Jun 16 19:48:10 2017 +0200 +++ b/hgext3rd/evolve/templatekw.py Sun Jul 02 17:28:38 2017 +0200 @@ -15,8 +15,10 @@ ) from mercurial import ( + cmdutil, templatekw, node, + util ) eh = exthelper.exthelper() @@ -126,7 +128,6 @@ datetempleq = ' (at {min_date|isodate})' datetemplnoteq = ' (between {min_date|isodate} and {max_date|isodate})' datetempl = '{if(max_date, "{ifeq(min_date, max_date, "%s", "%s")}")}' % (datetempleq, datetemplnoteq) - newline = '\n' optionalusertempl = usertempl username = _getusername(ui) @@ -136,15 +137,17 @@ # Assemble them return { - 'obsfate_quiet': verbtempl + succtempl + newline, - 'obsfate': verbtempl + optionalusertempl + succtempl + newline, - 'obsfate_verbose': verbtempl + usertempl + succtempl + datetempl + newline + 'obsfate_quiet': verbtempl + succtempl, + 'obsfate': verbtempl + optionalusertempl + succtempl, + 'obsfate_verbose': verbtempl + usertempl + succtempl + datetempl, } -@eh.templatekw("obsfate") -def showobsfate(repo, ctx, **args): +def obsfatedata(repo, ctx): + """compute the raw data needed for computing obsfate + Returns a list of dict + """ if not ctx.obsolete(): - return '' + return None successorssets, pathcache = closestsuccessors(repo, ctx.node()) @@ -173,7 +176,74 @@ values = [] for sset, rawmarkers in fullsuccessorsets: raw = obshistory.preparesuccessorset(sset, rawmarkers) + values.append(raw) + return values + +def obsfatelineprinter(obsfateline, ui): + quiet = ui.quiet + verbose = ui.verbose + normal = not verbose and not quiet + + # Build the line step by step + line = [] + + # Verb + line.append(obsfateline['verb']) + + # Users + if (verbose or normal) and 'users' in obsfateline: + users = obsfateline['users'] + + if normal: + username = _getusername(ui) + users = [user for user in users if user != username] + + if users: + line.append(" by %s" % ",".join(users)) + + # Successors + successors = obsfateline["successors"] + + if successors: + fmtsuccessors = map(lambda s: s[:12], successors) + line.append(" as %s" % ", ".join(fmtsuccessors)) + + # Date + if verbose: + min_date = obsfateline['min_date'] + max_date = obsfateline['max_date'] + + if min_date == max_date: + fmtmin_date = util.datestr(min_date, '%Y-%m-%d %H:%M %1%2') + line.append(" (at %s)" % fmtmin_date) + else: + fmtmin_date = util.datestr(min_date, '%Y-%m-%d %H:%M %1%2') + fmtmax_date = util.datestr(max_date, '%Y-%m-%d %H:%M %1%2') + line.append(" (between %s and %s)" % (fmtmin_date, fmtmax_date)) + + return "".join(line) + +def obsfateprinter(obsfate, ui, prefix=""): + lines = [] + for raw in obsfate: + lines.append(obsfatelineprinter(raw, ui)) + + if prefix: + lines = [prefix + line for line in lines] + + return "\n".join(lines) + +@eh.templatekw("obsfate") +def showobsfate(repo, ctx, **args): + # Get the needed obsfate data + values = obsfatedata(repo, ctx) + + if values is None: + return '' + + # Format each successorset successors list + for raw in values: # As we can't do something like # "{join(map(nodeshort, successors), ', '}" in template, manually # create a correct textual representation @@ -184,8 +254,7 @@ raw['successors'] = templatekw._hybrid(gen, raw['successors'], makemap, joinfmt) - values.append(raw) - + # And then format them # Insert default obsfate templates args['templ'].cache.update(obsfatedefaulttempl(repo.ui)) @@ -198,7 +267,45 @@ else: name = "obsfate" - return templatekw.showlist(name, values, args, separator=' + ') + # Format a single value + def fmt(d): + nargs = args.copy() + nargs.update(d[name]) + return args['templ'](name, **nargs) + + # Generate a good enough string representation using templater + gen = [] + for d in values: + chunk = fmt({name: d}) + chunkstr = [] + + # Empty the generator + try: + while True: + chunkstr.append(chunk.next()) + except StopIteration: + pass + + gen.append("".join(chunkstr)) + gen = "; ".join(gen) + + return templatekw._hybrid(gen, values, lambda x: {name: x}, fmt) + +# Check if we can hook directly on the changeset_printer +if util.safehasattr(cmdutil.changeset_printer, '_exthook'): + @eh.wrapfunction(cmdutil.changeset_printer, '_exthook') + def exthook(original, self, ctx): + # Call potential other extensions + original(self, ctx) + + obsfate = obsfatedata(self.repo, ctx) + if obsfate is None: + return "" + + output = obsfateprinter(obsfate, self.ui, prefix="obsolete: ") + + self.ui.write(output, label='log.obsfate') + self.ui.write("\n") # copy from mercurial.obsolete with a small change to stop at first known changeset. diff -r da0d3d4171cb -r 167567795f6c hgext3rd/topic/__init__.py --- a/hgext3rd/topic/__init__.py Fri Jun 16 19:48:10 2017 +0200 +++ b/hgext3rd/topic/__init__.py Sun Jul 02 17:28:38 2017 +0200 @@ -10,7 +10,7 @@ Compared to bookmark, topic is reference carried by each changesets of the series instead of just the single head revision. Topic are quite similar to the way named branch work, except they eventualy fade away when the changeset -becomes part of the immutable history. Changeset can below to both a topic and +becomes part of the immutable history. Changeset can belong to both a topic and a named branch, but as long as it is mutable, its topic identity will prevail. As a result, default destination for 'update', 'merge', etc... will take topic into account. When a topic is active these operations will only consider other @@ -21,10 +21,11 @@ There is currently two commands to be used with that extension: 'topics' and 'stack'. -The 'hg topics' command is used to set the current topic and list existing one. -'hg topics --verbose' will list various information related to each topic. +The 'hg topics' command is used to set the current topic, change and list +existing one. 'hg topics --verbose' will list various information related to +each topic. -The 'stack' will show you in formation about the stack of commit belonging to +The 'stack' will show you information about the stack of commit belonging to your current topic. Topic is offering you aliases reference to changeset in your current topic @@ -55,12 +56,12 @@ from mercurial.i18n import _ from mercurial import ( - branchmap, cmdutil, commands, context, error, extensions, + hg, localrepo, lock, merge, @@ -114,29 +115,41 @@ testedwith = '4.0.2 4.1.3 4.2' -def _contexttopic(self): +def _contexttopic(self, force=False): + if not (force or self.mutable()): + return '' return self.extra().get(constants.extrakey, '') context.basectx.topic = _contexttopic topicrev = re.compile(r'^t\d+$') +branchrev = re.compile(r'^b\d+$') def _namemap(repo, name): + revs = None if topicrev.match(name): idx = int(name[1:]) - topic = repo.currenttopic - if not topic: + ttype = 'topic' + tname = topic = repo.currenttopic + if not tname: raise error.Abort(_('cannot resolve "%s": no active topic') % name) - revs = list(stack.getstack(repo, topic)) + revs = list(stack.getstack(repo, topic=topic)) + elif branchrev.match(name): + ttype = 'branch' + idx = int(name[1:]) + tname = branch = repo[None].branch() + revs = list(stack.getstack(repo, branch=branch)) + + if revs is not None: try: r = revs[idx - 1] except IndexError: - msg = _('cannot resolve "%s": topic "%s" has only %d changesets') - raise error.Abort(msg % (name, topic, len(revs))) + msg = _('cannot resolve "%s": %s "%s" has only %d changesets') + raise error.Abort(msg % (name, ttype, tname, len(revs))) return [repo[r].node()] if name not in repo.topics: return [] - return [ctx.node() for ctx in - repo.set('not public() and extra(topic, %s)', name)] + node = repo.changelog.node + return [node(rev) for rev in repo.revs('topic(%s)', name)] def _nodemap(repo, node): ctx = repo[node] @@ -160,6 +173,13 @@ extensions.wrapfunction(cmdutil, 'buildcommittext', committextwrap) extensions.wrapfunction(merge, 'update', mergeupdatewrap) + + try: + evolve = extensions.find('evolve') + extensions.wrapfunction(evolve, "presplitupdate", presplitupdatetopic) + except (KeyError, AttributeError): + pass + cmdutil.summaryhooks.add('topic', summaryhook) @@ -167,6 +187,8 @@ if not isinstance(repo, localrepo.localrepository): return # this can be a peer in the ssh case (puzzling) + repo = repo.unfiltered() + if repo.ui.config('experimental', 'thg.displaynames', None) is None: repo.ui.setconfig('experimental', 'thg.displaynames', 'topics', source='topic-extension') @@ -189,6 +211,11 @@ self.ui.restoreconfig(backup) def commitctx(self, ctx, error=None): + topicfilter = topicmap.topicfilter(self.filtername) + if topicfilter != self.filtername: + other = repo.filtered(topicmap.topicfilter(repo.filtername)) + other.commitctx(ctx, error=error) + if isinstance(ctx, context.workingcommitctx): current = self.currenttopic if current: @@ -199,8 +226,7 @@ not self.currenttopic): # we are amending and need to remove a topic del ctx.extra()[constants.extrakey] - with topicmap.usetopicmap(self): - return super(topicrepo, self).commitctx(ctx, error=error) + return super(topicrepo, self).commitctx(ctx, error=error) @property def topics(self): @@ -217,23 +243,21 @@ def currenttopic(self): return self.vfs.tryread('topic') - def branchmap(self, topic=True): - if not topic: - super(topicrepo, self).branchmap() - with topicmap.usetopicmap(self): - branchmap.updatecache(self) - return self._topiccaches[self.filtername] + # overwritten at the instance level by topicmap.py + _autobranchmaptopic = True - def destroyed(self, *args, **kwargs): - with topicmap.usetopicmap(self): - return super(topicrepo, self).destroyed(*args, **kwargs) + def branchmap(self, topic=None): + if topic is None: + topic = getattr(repo, '_autobranchmaptopic', False) + topicfilter = topicmap.topicfilter(self.filtername) + if not topic or topicfilter == self.filtername: + return super(topicrepo, self).branchmap() + return self.filtered(topicfilter).branchmap() def invalidatevolatilesets(self): # XXX we might be able to move this to something invalidated less often super(topicrepo, self).invalidatevolatilesets() self._topics = None - if '_topiccaches' in vars(self.unfiltered()): - self.unfiltered()._topiccaches.clear() def peer(self): peer = super(topicrepo, self).peer() @@ -254,92 +278,39 @@ @command('topics [TOPIC]', [ ('', 'clear', False, 'clear active topic if any'), - ('', 'change', '', 'revset of existing revisions to change topic'), + ('r', 'rev', '', 'revset of existing revisions', _('REV')), ('l', 'list', False, 'show the stack of changeset in the topic'), ] + commands.formatteropts) -def topics(ui, repo, topic='', clear=False, change=None, list=False, **opts): +def topics(ui, repo, topic='', clear=False, rev=None, list=False, **opts): """View current topic, set current topic, or see all topics. The --verbose version of this command display various information on the state of each topic.""" if list: - if clear or change: - raise error.Abort(_("cannot use --clear or --change with --list")) + if clear or rev: + raise error.Abort(_("cannot use --clear or --rev with --list")) if not topic: topic = repo.currenttopic if not topic: raise error.Abort(_('no active topic to list')) - return stack.showstack(ui, repo, topic, opts) + return stack.showstack(ui, repo, topic=topic, opts=opts) - if change: + if rev: if not obsolete.isenabled(repo, obsolete.createmarkersopt): - raise error.Abort(_('must have obsolete enabled to use --change')) - if not topic and not clear: + raise error.Abort(_('must have obsolete enabled to change topics')) + if clear: + topic = None + elif not topic: raise error.Abort('changing topic requires a topic name or --clear') - if any(not c.mutable() for c in repo.set('%r and public()', change)): + if any(not c.mutable() for c in repo.set('%r and public()', rev)): raise error.Abort("can't change topic of a public change") - rewrote = 0 - needevolve = False - l = repo.lock() - txn = repo.transaction('rewrite-topics') - try: - for c in repo.set('%r', change): - def filectxfn(repo, ctx, path): - try: - return c[path] - except error.ManifestLookupError: - return None - fixedextra = dict(c.extra()) - ui.debug('old node id is %s\n' % node.hex(c.node())) - ui.debug('origextra: %r\n' % fixedextra) - newtopic = None if clear else topic - oldtopic = fixedextra.get(constants.extrakey, None) - if oldtopic == newtopic: - continue - if clear: - del fixedextra[constants.extrakey] - else: - fixedextra[constants.extrakey] = topic - if 'amend_source' in fixedextra: - # TODO: right now the commitctx wrapper in - # topicrepo overwrites the topic in extra if - # amend_source is set to support 'hg commit - # --amend'. Support for amend should be adjusted - # to not be so invasive. - del fixedextra['amend_source'] - ui.debug('changing topic of %s from %s to %s\n' % ( - c, oldtopic, newtopic)) - ui.debug('fixedextra: %r\n' % fixedextra) - mc = context.memctx( - repo, (c.p1().node(), c.p2().node()), c.description(), - c.files(), filectxfn, - user=c.user(), date=c.date(), extra=fixedextra) - newnode = repo.commitctx(mc) - ui.debug('new node id is %s\n' % node.hex(newnode)) - needevolve = needevolve or (len(c.children()) > 0) - obsolete.createmarkers(repo, [(c, (repo[newnode],))]) - rewrote += 1 - txn.close() - except: - try: - txn.abort() - finally: - repo.invalidate() - raise - finally: - lock.release(txn, l) - ui.status('changed topic on %d changes\n' % rewrote) - if needevolve: - evolvetarget = 'topic(%s)' % topic if topic else 'not topic()' - ui.status('please run hg evolve --rev "%s" now\n' % evolvetarget) + return _changetopics(ui, repo, rev, topic) + if clear: - if repo.vfs.exists('topic'): - repo.vfs.unlink('topic') - return + return _changecurrenttopic(repo, None) + if topic: - with repo.wlock(): - with repo.vfs.open('topic', 'w') as f: - f.write(topic) - return + return _changecurrenttopic(repo, topic) + _listtopics(ui, repo, opts) @command('stack [TOPIC]', [] + commands.formatteropts) @@ -348,13 +319,95 @@ List the current topic by default.""" if not topic: + topic = None + branch = None + if topic is None and repo.currenttopic: topic = repo.currenttopic - if not topic: - raise error.Abort(_('no active topic to list')) - return stack.showstack(ui, repo, topic, opts) + if topic is None: + branch = repo[None].branch() + return stack.showstack(ui, repo, branch=branch, topic=topic, opts=opts) + +def _changecurrenttopic(repo, newtopic): + """changes the current topic.""" + + if newtopic: + with repo.wlock(): + with repo.vfs.open('topic', 'w') as f: + f.write(newtopic) + else: + if repo.vfs.exists('topic'): + repo.vfs.unlink('topic') + +def _changetopics(ui, repo, revset, newtopic): + rewrote = 0 + wl = l = txn = None + try: + wl = repo.wlock() + l = repo.lock() + txn = repo.transaction('rewrite-topics') + p1 = None + p2 = None + successors = {} + for c in repo.set('%r', revset): + def filectxfn(repo, ctx, path): + try: + return c[path] + except error.ManifestLookupError: + return None + fixedextra = dict(c.extra()) + ui.debug('old node id is %s\n' % node.hex(c.node())) + ui.debug('origextra: %r\n' % fixedextra) + oldtopic = fixedextra.get(constants.extrakey, None) + if oldtopic == newtopic: + continue + if newtopic is None: + del fixedextra[constants.extrakey] + else: + fixedextra[constants.extrakey] = newtopic + fixedextra[constants.changekey] = c.hex() + if 'amend_source' in fixedextra: + # TODO: right now the commitctx wrapper in + # topicrepo overwrites the topic in extra if + # amend_source is set to support 'hg commit + # --amend'. Support for amend should be adjusted + # to not be so invasive. + del fixedextra['amend_source'] + ui.debug('changing topic of %s from %s to %s\n' % ( + c, oldtopic, newtopic)) + ui.debug('fixedextra: %r\n' % fixedextra) + # While changing topic of set of linear commits, make sure that + # we base our commits on new parent rather than old parent which + # was obsoleted while changing the topic + p1 = c.p1().node() + p2 = c.p2().node() + if p1 in successors: + p1 = successors[p1] + if p2 in successors: + p2 = successors[p2] + mc = context.memctx( + repo, (p1, p2), c.description(), + c.files(), filectxfn, + user=c.user(), date=c.date(), extra=fixedextra) + newnode = repo.commitctx(mc) + successors[c.node()] = newnode + ui.debug('new node id is %s\n' % node.hex(newnode)) + obsolete.createmarkers(repo, [(c, (repo[newnode],))]) + rewrote += 1 + # move the working copy too + wctx = repo[None] + # in-progress merge is a bit too complex for now. + if len(wctx.parents()) == 1: + newid = successors.get(wctx.p1().node()) + if newid is not None: + hg.update(repo, newid, quietempty=True) + txn.close() + finally: + lock.release(txn, l, wl) + repo.invalidate() + ui.status('changed topic on %d changes\n' % rewrote) def _listtopics(ui, repo, opts): - fm = ui.formatter('bookmarks', opts) + fm = ui.formatter('topics', opts) activetopic = repo.currenttopic namemask = '%s' if repo.topics and ui.verbose: @@ -375,7 +428,7 @@ fm.data(active=active) if ui.verbose: # XXX we should include the data even when not verbose - data = stack.stackdata(repo, topic) + data = stack.stackdata(repo, topic=topic) fm.plain(' (') fm.write('branches+', 'on branch: %s', '+'.join(data['branches']), # XXX use list directly after 4.0 is released @@ -434,9 +487,17 @@ matcher = kwargs.get('matcher') partial = not (matcher is None or matcher.always()) wlock = repo.wlock() + isrebase = False try: ret = orig(repo, node, branchmerge, force, *args, **kwargs) - if not partial and not branchmerge: + # The mergeupdatewrap function makes the destination's topic as the + # current topic. This is right for merge but wrong for rebase. We check + # if rebase is running and update the currenttopic to topic of new + # rebased commit. We have explicitly stored in config if rebase is + # running. + if repo.ui.hasconfig('experimental', 'topicrebase'): + isrebase = True + if (not partial and not branchmerge) or isrebase: ot = repo.currenttopic t = '' pctx = repo[node] @@ -461,9 +522,18 @@ def newmakeextrafn(orig, copiers): return orig(copiers + [savetopic]) + def setrebaseconfig(orig, ui, repo, **opts): + repo.ui.setconfig('experimental', 'topicrebase', 'yes', + source='topic-extension') + return orig(ui, repo, **opts) + try: rebase = extensions.find("rebase") extensions.wrapfunction(rebase, '_makeextrafn', newmakeextrafn) + # This exists to store in the config that rebase is running so that we can + # update the topic according to rebase. This is a hack and should be removed + # when we have better options. + extensions.wrapcommand(rebase.cmdtable, 'rebase', setrebaseconfig) except KeyError: pass @@ -486,3 +556,18 @@ cmdutil.extrapreimport.append('topic') cmdutil.extrapreimportmap['topic'] = _importtopic patch.patchheadermap.append(('EXP-Topic', 'topic')) + +## preserve topic during split + +def presplitupdatetopic(original, repo, ui, prev, ctx): + # Save topic of revision + topic = None + if util.safehasattr(ctx, 'topic'): + topic = ctx.topic() + + # Update the working directory + original(repo, ui, prev, ctx) + + # Restore the topic if need + if topic: + _changecurrenttopic(repo, topic) diff -r da0d3d4171cb -r 167567795f6c hgext3rd/topic/constants.py --- a/hgext3rd/topic/constants.py Fri Jun 16 19:48:10 2017 +0200 +++ b/hgext3rd/topic/constants.py Sun Jul 02 17:28:38 2017 +0200 @@ -1,1 +1,2 @@ extrakey = 'topic' +changekey = '_rewrite_noise' diff -r da0d3d4171cb -r 167567795f6c hgext3rd/topic/destination.py --- a/hgext3rd/topic/destination.py Fri Jun 16 19:48:10 2017 +0200 +++ b/hgext3rd/topic/destination.py Sun Jul 02 17:28:38 2017 +0200 @@ -89,14 +89,14 @@ # but that is expensive # # we should write plain code instead - with topicmap.usetopicmap(repo): - tmap = repo.branchmap() - if branch not in tmap: - return [] - elif all: - return tmap.branchheads(branch) - else: - return [tmap.branchtip(branch)] + + tmap = topicmap.gettopicrepo(repo).branchmap() + if branch not in tmap: + return [] + elif all: + return tmap.branchheads(branch) + else: + return [tmap.branchtip(branch)] def modsetup(ui): """run a uisetup time to install all destinations wrapping""" diff -r da0d3d4171cb -r 167567795f6c hgext3rd/topic/discovery.py --- a/hgext3rd/topic/discovery.py Fri Jun 16 19:48:10 2017 +0200 +++ b/hgext3rd/topic/discovery.py Sun Jul 02 17:28:38 2017 +0200 @@ -4,24 +4,24 @@ from mercurial.i18n import _ from mercurial import ( - branchmap, bundle2, discovery, error, exchange, extensions, + util, wireproto, ) -from . import topicmap - def _headssummary(orig, *args): # In mercurial < 4.2, we receive repo, remote and outgoing as arguments if len(args) == 3: + pushoparg = False repo, remote, outgoing = args # In mercurial > 4.3, we receive the pushop as arguments elif len(args) == 1: + pushoparg = True pushop = args[0] repo = pushop.repo.unfiltered() remote = pushop.remote @@ -33,38 +33,56 @@ or bool(remote.listkeys('phases').get('publishing', False))) if publishing or not remote.capable('topics'): return orig(*args) - oldrepo = repo.__class__ - oldbranchcache = branchmap.branchcache - oldfilename = branchmap._filename - try: - class repocls(repo.__class__): - def __getitem__(self, key): - ctx = super(repocls, self).__getitem__(key) - oldbranch = ctx.branch + + class repocls(repo.__class__): + # awful hack to see branch as "branch:topic" + def __getitem__(self, key): + ctx = super(repocls, self).__getitem__(key) + oldbranch = ctx.branch + + def branch(): + branch = oldbranch() + topic = ctx.topic() + if topic: + branch = "%s:%s" % (branch, topic) + return branch + + ctx.branch = branch + return ctx + + def revbranchcache(self): + rbc = super(repocls, self).revbranchcache() + changelog = self.changelog - def branch(): - branch = oldbranch() - topic = ctx.topic() - if topic: - branch = "%s:%s" % (branch, topic) - return branch + def branchinfo(rev): + branch, close = changelog.branchinfo(rev) + topic = repo[rev].topic() + if topic: + branch = "%s:%s" % (branch, topic) + return branch, close + + rbc.branchinfo = branchinfo + return rbc - ctx.branch = branch - return ctx - + oldrepocls = repo.__class__ + try: repo.__class__ = repocls - branchmap.branchcache = topicmap.topiccache - branchmap._filename = topicmap._filename - summary = orig(*args) + unxx = repo.filtered('unfiltered-topic') + repo.unfiltered = lambda: unxx + if pushoparg: + pushop.repo = repo + summary = orig(pushop) + else: + summary = orig(repo, remote, outgoing) for key, value in summary.iteritems(): if ':' in key: # This is a topic if value[0] is None and value[1]: - summary[key] = ([value[1].pop(0)], ) + value[1:] + summary[key] = ([value[1][0]], ) + value[1:] return summary finally: - repo.__class__ = oldrepo - branchmap.branchcache = oldbranchcache - branchmap._filename = oldfilename + if 'unfiltered' in vars(repo): + del repo.unfiltered + repo.__class__ = oldrepocls def wireprotobranchmap(orig, repo, proto): oldrepo = repo.__class__ @@ -94,6 +112,7 @@ return data def handlecheckheads(orig, op, inpart): + """This is used to check for new heads when publishing changeset""" orig(op, inpart) if op.repo.publishing(): return @@ -124,8 +143,9 @@ handlecheckheads.params = frozenset() def _pushb2phases(orig, pushop, bundler): - hascheck = any(p.type == 'check:heads' for p in bundler._parts) - if pushop.outdatedphases and not hascheck: + checktypes = ('check:heads', 'check:updated-heads') + hascheck = any(p.type in checktypes for p in bundler._parts) + if not hascheck and pushop.outdatedphases: exchange._pushb2ctxcheckheads(pushop, bundler) return orig(pushop, bundler) @@ -140,9 +160,14 @@ extensions.wrapfunction(discovery, '_headssummary', _headssummary) extensions.wrapfunction(wireproto, 'branchmap', wireprotobranchmap) extensions.wrapfunction(wireproto, '_capabilities', wireprotocaps) + # we need a proper wrap b2 part stuff extensions.wrapfunction(bundle2, 'handlecheckheads', handlecheckheads) - # we need a proper wrap b2 part stuff bundle2.handlecheckheads.params = frozenset() bundle2.parthandlermapping['check:heads'] = bundle2.handlecheckheads + if util.safehasattr(bundle2, 'handlecheckupdatedheads'): + # we still need a proper wrap b2 part stuff + extensions.wrapfunction(bundle2, 'handlecheckupdatedheads', handlecheckheads) + bundle2.handlecheckupdatedheads.params = frozenset() + bundle2.parthandlermapping['check:updated-heads'] = bundle2.handlecheckupdatedheads extensions.wrapfunction(exchange, '_pushb2phases', _pushb2phases) exchange.b2partsgenmapping['phase'] = exchange._pushb2phases diff -r da0d3d4171cb -r 167567795f6c hgext3rd/topic/evolvebits.py --- a/hgext3rd/topic/evolvebits.py Fri Jun 16 19:48:10 2017 +0200 +++ b/hgext3rd/topic/evolvebits.py Sun Jul 02 17:28:38 2017 +0200 @@ -1,6 +1,16 @@ import collections from mercurial import obsolete +successorssets = None +try: + from mercurial import obsutil + successorssets = getattr(obsutil, 'successorssets', None) +except ImportError: + pass + +if successorssets is None: + successorssets = obsolete.successorssets + # Copied from evolve 081605c2e9b6 def _orderrevs(repo, revs): @@ -72,14 +82,14 @@ return p.rev() obs = repo[p] ui = repo.ui - newer = obsolete.successorssets(repo, obs.node()) + newer = successorssets(repo, obs.node()) # search of a parent which is not killed while not newer: ui.debug("stabilize target %s is plain dead," " trying to stabilize on its parent\n" % obs) obs = obs.parents()[0] - newer = obsolete.successorssets(repo, obs.node()) + newer = successorssets(repo, obs.node()) if len(newer) > 1 or len(newer[0]) > 1: raise MultipleSuccessorsError(newer) diff -r da0d3d4171cb -r 167567795f6c hgext3rd/topic/revset.py --- a/hgext3rd/topic/revset.py Fri Jun 16 19:48:10 2017 +0200 +++ b/hgext3rd/topic/revset.py Sun Jul 02 17:28:38 2017 +0200 @@ -1,8 +1,6 @@ from __future__ import absolute_import -from mercurial.i18n import _ from mercurial import ( - error, revset, util, ) @@ -37,9 +35,18 @@ _kind, _pattern, matcher = mkmatcher(topic) else: matcher = lambda t: bool(t) - drafts = subset.filter(lambda r: repo[r].mutable()) - return drafts.filter( - lambda r: matcher(repo[r].extra().get(constants.extrakey, ''))) + + mutable = revset._notpublic(repo, revset.fullreposet(repo), ()) + + rawchange = repo.changelog.changelogrevision + key = constants.extrakey + + def matchtopic(r): + topic = rawchange(r).extra.get(key) + if topic is None: + return False + return matcher(topic) + return (subset & mutable).filter(matchtopic) def ngtipset(repo, subset, x): """`ngtip([branch])` @@ -62,11 +69,16 @@ This is roughly equivalent to 'topic(.) - obsolete' with a sorting moving unstable changeset after there future parent (as if evolve where already run).""" + err = 'stack() takes no argument, it works on current topic' + revset.getargs(x, 0, 0, err) topic = repo.currenttopic + topic = None + branch = None + if not topic and repo.currenttopic: + topic = repo.currenttopic if not topic: - raise error.Abort(_('no active topic to list')) - # ordering hack, boo - return revset.baseset(stack.getstack(repo, topic)) & subset + branch = repo[None].branch() + return revset.baseset(stack.getstack(repo, branch=branch, topic=topic)) & subset def modsetup(ui): diff -r da0d3d4171cb -r 167567795f6c hgext3rd/topic/stack.py --- a/hgext3rd/topic/stack.py Fri Jun 16 19:48:10 2017 +0200 +++ b/hgext3rd/topic/stack.py Sun Jul 02 17:28:38 2017 +0200 @@ -10,9 +10,16 @@ ) from .evolvebits import builddependencies, _orderrevs, _singlesuccessor -def getstack(repo, topic): +def getstack(repo, branch=None, topic=None): # XXX need sorting - trevs = repo.revs("topic(%s) - obsolete()", topic) + if topic is not None and branch is not None: + raise error.ProgrammingError('both branch and topic specified (not defined yet)') + elif topic is not None: + trevs = repo.revs("topic(%s) - obsolete()", topic) + elif branch is not None: + trevs = repo.revs("branch(%s) - public() - obsolete() - topic()", branch) + else: + raise error.ProgrammingError('neither branch and topic specified (not defined yet)') return _orderrevs(repo, trevs) def labelsgen(prefix, labelssuffix): @@ -21,7 +28,23 @@ """ return ' '.join(prefix % suffix for suffix in labelssuffix) -def showstack(ui, repo, topic, opts): +def showstack(ui, repo, branch=None, topic=None, opts=None): + if opts is None: + opts = {} + + if topic is not None and branch is not None: + msg = 'both branch and topic specified [%s]{%s}(not defined yet)' + msg %= (branch, topic) + raise error.ProgrammingError(msg) + elif topic is not None: + prefix = 't' + if topic not in repo.topics: + raise error.Abort(_('cannot resolve "%s": no such topic found') % topic) + elif branch is not None: + prefix = 'b' + else: + raise error.ProgrammingError('neither branch and topic specified (not defined yet)') + fm = ui.formatter('topicstack', opts) prev = None entries = [] @@ -31,28 +54,37 @@ if topic == repo.currenttopic: label = 'topic.active' - data = stackdata(repo, topic) - fm.plain(_('### topic: %s') % ui.label(topic, label), - label='topic.stack.summary.topic') + data = stackdata(repo, branch=branch, topic=topic) + if topic is not None: + fm.plain(_('### topic: %s') + % ui.label(topic, label), + label='topic.stack.summary.topic') - if 1 < data['headcount']: - fm.plain(' (') - fm.plain('%d heads' % data['headcount'], - label='topic.stack.summary.headcount.multiple') - fm.plain(')') - fm.plain('\n') + if 1 < data['headcount']: + fm.plain(' (') + fm.plain('%d heads' % data['headcount'], + label='topic.stack.summary.headcount.multiple') + fm.plain(')') + fm.plain('\n') fm.plain(_('### branch: %s') % '+'.join(data['branches']), # XXX handle multi branches label='topic.stack.summary.branches') - if data['behindcount'] == -1: - fm.plain(', ') - fm.plain('ambigious rebase destination', label='topic.stack.summary.behinderror') - elif data['behindcount']: - fm.plain(', ') - fm.plain('%d behind' % data['behindcount'], label='topic.stack.summary.behindcount') + if topic is None: + if 1 < data['headcount']: + fm.plain(' (') + fm.plain('%d heads' % data['headcount'], + label='topic.stack.summary.headcount.multiple') + fm.plain(')') + else: + if data['behindcount'] == -1: + fm.plain(', ') + fm.plain('ambigious rebase destination', label='topic.stack.summary.behinderror') + elif data['behindcount']: + fm.plain(', ') + fm.plain('%d behind' % data['behindcount'], label='topic.stack.summary.behindcount') fm.plain('\n') - for idx, r in enumerate(getstack(repo, topic), 1): + for idx, r in enumerate(getstack(repo, branch=branch, topic=topic), 1): ctx = repo[r] p1 = ctx.p1() if p1.obsolete(): @@ -69,19 +101,22 @@ states = [] iscurrentrevision = repo.revs('%d and parents()', ctx.rev()) - if iscurrentrevision: - states.append('current') - if not isentry: symbol = '^' # "base" is kind of a "ghost" entry # skip other label for them (no current, no unstable) states = ['base'] - elif iscurrentrevision: - symbol = '@' - elif repo.revs('%d and unstable()', ctx.rev()): + elif ctx.unstable(): + # current revision can be unstable also, so in that case show both + # the states and the symbol '@' (issue5553) + if iscurrentrevision: + states.append('current') + symbol = '@' symbol = '$' states.append('unstable') + elif iscurrentrevision: + states.append('current') + symbol = '@' else: symbol = ':' states.append('clean') @@ -91,7 +126,7 @@ if idx is None: fm.plain(' ') else: - fm.write('topic.stack.index', 't%d', idx, + fm.write('topic.stack.index', '%s%%d' % prefix, idx, label='topic.stack.index ' + labelsgen('topic.stack.index.%s', states)) fm.write('topic.stack.state.symbol', '%s', symbol, label='topic.stack.state ' + labelsgen('topic.stack.state.%s', states)) @@ -104,7 +139,7 @@ fm.plain('\n') fm.end() -def stackdata(repo, topic): +def stackdata(repo, branch=None, topic=None): """get various data about a stack :changesetcount: number of non-obsolete changesets in the stack @@ -113,7 +148,7 @@ :behindcount: number of changeset on rebase destination """ data = {} - revs = repo.revs("topic(%s) - obsolete()", topic) + revs = getstack(repo, branch, topic) data['changesetcount'] = len(revs) data['troubledcount'] = len([r for r in revs if repo[r].troubled()]) deps, rdeps = builddependencies(repo, revs) diff -r da0d3d4171cb -r 167567795f6c hgext3rd/topic/topicmap.py --- a/hgext3rd/topic/topicmap.py Fri Jun 16 19:48:10 2017 +0200 +++ b/hgext3rd/topic/topicmap.py Sun Jul 02 17:28:38 2017 +0200 @@ -1,27 +1,63 @@ import contextlib import hashlib -from mercurial.node import hex, bin, nullid +from mercurial.node import nullid from mercurial import ( branchmap, changegroup, cmdutil, - encoding, - error, extensions, - scmutil, + repoview, ) -def _filename(repo): - """name of a branchcache file for a given repo or repoview""" - filename = "cache/topicmap" - if repo.filtername: - filename = '%s-%s' % (filename, repo.filtername) - return filename +basefilter = set(['base', 'immutable']) +def topicfilter(name): + """return a "topic" version of a filter level""" + if name in basefilter: + return name + elif name is None: + return None + elif name.endswith('-topic'): + return name + else: + return name + '-topic' + +def istopicfilter(filtername): + if filtername is None: + return False + return filtername.endswith('-topic') + +def gettopicrepo(repo): + filtername = topicfilter(repo.filtername) + if filtername == repo.filtername: + return repo + return repo.filtered(filtername) -oldbranchcache = branchmap.branchcache +def _setuptopicfilter(ui): + """extend the filter related mapping with topic related one""" + funcmap = repoview.filtertable + partialmap = branchmap.subsettable + + # filter level not affected by topic that we should not override + + for plainname in list(funcmap): + newfilter = topicfilter(plainname) + if newfilter == plainname: + continue + + def revsfunc(repo, name=plainname): + return repoview.filterrevs(repo, name) + + base = topicfilter(partialmap[plainname]) + + if newfilter not in funcmap: + funcmap[newfilter] = revsfunc + partialmap[newfilter] = base + funcmap['unfiltered-topic'] = lambda repo: frozenset() + partialmap['unfiltered-topic'] = 'visible-topic' def _phaseshash(repo, maxrev): + """uniq ID for a phase matching a set of rev""" revs = set() cl = repo.changelog fr = cl.filteredrevs @@ -40,61 +76,65 @@ key = s.digest() return key -@contextlib.contextmanager -def usetopicmap(repo): - """use awful monkey patching to ensure topic map usage +def modsetup(ui): + """call at uisetup time to install various wrappings""" + _setuptopicfilter(ui) + _wrapbmcache(ui) + extensions.wrapfunction(changegroup.cg1unpacker, 'apply', cgapply) + extensions.wrapfunction(cmdutil, 'commitstatus', commitstatus) - During the extend of the context block, The topicmap should be used and - updated instead of the branchmap.""" - oldbranchcache = branchmap.branchcache - oldfilename = branchmap._filename - oldread = branchmap.read - oldcaches = getattr(repo, '_branchcaches', {}) - try: - branchmap.branchcache = topiccache - branchmap._filename = _filename - branchmap.read = readtopicmap - repo._branchcaches = getattr(repo, '_topiccaches', {}) - yield - repo._topiccaches = repo._branchcaches - finally: - repo._branchcaches = oldcaches - branchmap.branchcache = oldbranchcache - branchmap._filename = oldfilename - branchmap.read = oldread - -def cgapply(orig, repo, *args, **kwargs): +def cgapply(orig, self, repo, *args, **kwargs): """make sure a topicmap is used when applying a changegroup""" - with usetopicmap(repo): - return orig(repo, *args, **kwargs) + other = repo.filtered(topicfilter(repo.filtername)) + return orig(self, other, *args, **kwargs) def commitstatus(orig, repo, node, branch, bheads=None, opts=None): # wrap commit status use the topic branch heads ctx = repo[node] if ctx.topic() and ctx.branch() == branch: - bheads = repo.branchheads("%s:%s" % (branch, ctx.topic())) + subbranch = "%s:%s" % (branch, ctx.topic()) + bheads = repo.branchheads("%s:%s" % (subbranch, ctx.topic())) return orig(repo, node, branch, bheads=bheads, opts=opts) -class topiccache(oldbranchcache): +def _wrapbmcache(ui): + class topiccache(_topiccache, branchmap.branchcache): + pass + branchmap.branchcache = topiccache + extensions.wrapfunction(branchmap, 'updatecache', _wrapupdatebmcache) + +def _wrapupdatebmcache(orig, repo): + previous = getattr(repo, '_autobranchmaptopic', False) + try: + repo._autobranchmaptopic = False + return orig(repo) + finally: + repo._autobranchmaptopic = previous + +# needed to prevent reference used for 'super()' call using in branchmap.py to +# no go into cycle. (yes, URG) +_oldbranchmap = branchmap.branchcache + +@contextlib.contextmanager +def oldbranchmap(): + previous = branchmap.branchcache + try: + branchmap.branchcache = _oldbranchmap + yield + finally: + branchmap.branchcache = previous + +class _topiccache(object): # combine me with branchmap.branchcache def __init__(self, *args, **kwargs): - otherbranchcache = branchmap.branchcache - try: - # super() call may fail otherwise - branchmap.branchcache = oldbranchcache - super(topiccache, self).__init__(*args, **kwargs) - if self.filteredhash is None: - self.filteredhash = nullid - self.phaseshash = nullid - finally: - branchmap.branchcache = otherbranchcache + # super() call may fail otherwise + with oldbranchmap(): + super(_topiccache, self).__init__(*args, **kwargs) + self.phaseshash = None def copy(self): """return an deep copy of the branchcache object""" - new = topiccache(self, self.tipnode, self.tiprev, self.filteredhash, - self._closednodes) - if self.filteredhash is None: - self.filteredhash = nullid + new = self.__class__(self, self.tipnode, self.tiprev, self.filteredhash, + self._closednodes) new.phaseshash = self.phaseshash return new @@ -104,144 +144,61 @@ Raise KeyError for unknown branch.''' if topic: branch = '%s:%s' % (branch, topic) - return super(topiccache, self).branchtip(branch) + return super(_topiccache, self).branchtip(branch) def branchheads(self, branch, closed=False, topic=''): if topic: branch = '%s:%s' % (branch, topic) - return super(topiccache, self).branchheads(branch, closed=closed) + return super(_topiccache, self).branchheads(branch, closed=closed) def validfor(self, repo): """Is the cache content valid regarding a repo - False when cached tipnode is unknown or if we detect a strip. - True when cache is up to date or a subset of current repo.""" - # This is copy paste of mercurial.branchmap.branchcache.validfor in - # 69077c65919d With a small changes to the cache key handling to - # include phase information that impact the topic cache. - # - # All code changes should be flagged on site. - try: - if (self.tipnode == repo.changelog.node(self.tiprev)): - fh = scmutil.filteredhash(repo, self.tiprev) - if fh is None: - fh = nullid - if ((self.filteredhash == fh) - and (self.phaseshash == _phaseshash(repo, self.tiprev))): - return True + valid = super(_topiccache, self).validfor(repo) + if not valid: return False - except IndexError: - return False + elif not istopicfilter(repo.filtername) or self.phaseshash is None: + # phasehash at None means this is a branchmap + # come from non topic thing + return True + else: + try: + valid = self.phaseshash == _phaseshash(repo, self.tiprev) + return valid + except IndexError: + return False def write(self, repo): - # This is copy paste of mercurial.branchmap.branchcache.write in - # 69077c65919d With a small changes to the cache key handling to - # include phase information that impact the topic cache. - # - # All code changes should be flagged on site. - try: - f = repo.vfs(_filename(repo), "w", atomictemp=True) - cachekey = [hex(self.tipnode), str(self.tiprev)] - # [CHANGE] we need a hash in all cases - assert self.filteredhash is not None - cachekey.append(hex(self.filteredhash)) - cachekey.append(hex(self.phaseshash)) - f.write(" ".join(cachekey) + '\n') - nodecount = 0 - for label, nodes in sorted(self.iteritems()): - for node in nodes: - nodecount += 1 - if node in self._closednodes: - state = 'c' - else: - state = 'o' - f.write("%s %s %s\n" % (hex(node), state, - encoding.fromlocal(label))) - f.close() - repo.ui.log('branchcache', - 'wrote %s branch cache with %d labels and %d nodes\n', - repo.filtername, len(self), nodecount) - except (IOError, OSError, error.Abort) as inst: - repo.ui.debug("couldn't write branch cache: %s\n" % inst) - # Abort may be raise by read only opener - pass + # we expect mutable set to be small enough to be that computing it all + # the time will be fast enough + if not istopicfilter(repo.filtername): + super(_topiccache, self).write(repo) def update(self, repo, revgen): """Given a branchhead cache, self, that may have extra nodes or be missing heads, and a generator of nodes that are strictly a superset of heads missing, this function updates self to be correct. """ - oldgetbranchinfo = repo.revbranchcache().branchinfo + if not istopicfilter(repo.filtername): + return super(_topiccache, self).update(repo, revgen) + unfi = repo.unfiltered() + oldgetbranchinfo = unfi.revbranchcache().branchinfo + + def branchinfo(r): + info = oldgetbranchinfo(r) + topic = '' + ctx = unfi[r] + if ctx.mutable(): + topic = ctx.topic() + branch = info[0] + if topic: + branch = '%s:%s' % (branch, topic) + return (branch, info[1]) try: - def branchinfo(r): - info = oldgetbranchinfo(r) - topic = '' - ctx = repo[r] - if ctx.mutable(): - topic = ctx.topic() - branch = info[0] - if topic: - branch = '%s:%s' % (branch, topic) - return (branch, info[1]) - repo.revbranchcache().branchinfo = branchinfo - super(topiccache, self).update(repo, revgen) - if self.filteredhash is None: - self.filteredhash = nullid + unfi.revbranchcache().branchinfo = branchinfo + super(_topiccache, self).update(repo, revgen) self.phaseshash = _phaseshash(repo, self.tiprev) finally: - repo.revbranchcache().branchinfo = oldgetbranchinfo - -def readtopicmap(repo): - # This is copy paste of mercurial.branchmap.read in 69077c65919d - # With a small changes to the cache key handling to include phase - # information that impact the topic cache. - # - # All code changes should be flagged on site. - try: - f = repo.vfs(_filename(repo)) - lines = f.read().split('\n') - f.close() - except (IOError, OSError): - return None - - try: - cachekey = lines.pop(0).split(" ", 2) - last, lrev = cachekey[:2] - last, lrev = bin(last), int(lrev) - filteredhash = bin(cachekey[2]) # [CHANGE] unconditional filteredhash - partial = topiccache(tipnode=last, tiprev=lrev, - filteredhash=filteredhash) - partial.phaseshash = bin(cachekey[3]) # [CHANGE] read phaseshash - if not partial.validfor(repo): - # invalidate the cache - raise ValueError('tip differs') - cl = repo.changelog - for l in lines: - if not l: - continue - node, state, label = l.split(" ", 2) - if state not in 'oc': - raise ValueError('invalid branch state') - label = encoding.tolocal(label.strip()) - node = bin(node) - if not cl.hasnode(node): - raise ValueError('node %s does not exist' % hex(node)) - partial.setdefault(label, []).append(node) - if state == 'c': - partial._closednodes.add(node) - except KeyboardInterrupt: - raise - except Exception as inst: - if repo.ui.debugflag: - msg = 'invalid branchheads cache' - if repo.filtername is not None: - msg += ' (%s)' % repo.filtername - msg += ': %s\n' - repo.ui.debug(msg % inst) - partial = None - return partial - -def modsetup(ui): - """call at uisetup time to install various wrappings""" - extensions.wrapfunction(changegroup.cg1unpacker, 'apply', cgapply) - extensions.wrapfunction(cmdutil, 'commitstatus', commitstatus) + unfi.revbranchcache().branchinfo = oldgetbranchinfo diff -r da0d3d4171cb -r 167567795f6c tests/test-check-commit.t --- a/tests/test-check-commit.t Fri Jun 16 19:48:10 2017 +0200 +++ b/tests/test-check-commit.t Sun Jul 02 17:28:38 2017 +0200 @@ -3,6 +3,8 @@ Enable obsolescence to avoid the warning issue when obsmarker are found $ cat << EOF >> $HGRCPATH + > [diff] + > git = yes > [experimental] > evolution=all > EOF diff -r da0d3d4171cb -r 167567795f6c tests/test-discovery-obshashrange.t diff -r da0d3d4171cb -r 167567795f6c tests/test-drop.t diff -r da0d3d4171cb -r 167567795f6c tests/test-evolve-templates.t diff -r da0d3d4171cb -r 167567795f6c tests/test-inhibit.t diff -r da0d3d4171cb -r 167567795f6c tests/test-metaedit.t diff -r da0d3d4171cb -r 167567795f6c tests/test-obsolete.t diff -r da0d3d4171cb -r 167567795f6c tests/test-sharing.t diff -r da0d3d4171cb -r 167567795f6c tests/test-split.t --- a/tests/test-split.t Fri Jun 16 19:48:10 2017 +0200 +++ b/tests/test-split.t Sun Jul 02 17:28:38 2017 +0200 @@ -24,7 +24,7 @@ $ mkcommit() { > echo "$1" > "$1" > hg add "$1" - > hg ci -m "add $1" + > hg ci -m "add $1" $2 $3 > } @@ -33,13 +33,13 @@ $ cd testsplit $ mkcommit _a $ mkcommit _b - $ mkcommit _c + $ mkcommit _c --user other-test-user $ mkcommit _d $ echo "change to a" >> _a $ hg amend $ hg debugobsolete - 9e84a109b8eb081ad754681ee4b1380d17a3741f aa8f656bb307022172d2648be6fb65322f801225 0 (*) {'ef1': '*', 'user': 'test'} (glob) - f002b57772d7f09b180c407213ae16d92996a988 0 {9e84a109b8eb081ad754681ee4b1380d17a3741f} (*) {'ef1': '*', 'user': 'test'} (glob) + 1334a80b33c3f9873edab728fbbcf500eab61d2e d2fe56e71366c2c5376c89960c281395062c0619 0 (*) {'ef1': '8', 'user': 'test'} (glob) + 06be89dfe2ae447383f30a2984933352757b6fb4 0 {1334a80b33c3f9873edab728fbbcf500eab61d2e} (*) {'ef1': '0', 'user': 'test'} (glob) To create commits with the number of split $ echo 0 > num @@ -91,24 +91,24 @@ no more change to split $ hg debugobsolete - 9e84a109b8eb081ad754681ee4b1380d17a3741f aa8f656bb307022172d2648be6fb65322f801225 0 (*) {'ef1': '*', 'user': 'test'} (glob) - f002b57772d7f09b180c407213ae16d92996a988 0 {9e84a109b8eb081ad754681ee4b1380d17a3741f} (*) {'ef1': '*', 'user': 'test'} (glob) - aa8f656bb307022172d2648be6fb65322f801225 a98b35e86cae589b61892127c5ec1c868e41d910 5410a2352fa3114883327beee89e3085eefac25c 0 (*) {'ef1': '*', 'user': 'test'} (glob) + 1334a80b33c3f9873edab728fbbcf500eab61d2e d2fe56e71366c2c5376c89960c281395062c0619 0 (*) {'ef1': '8', 'user': 'test'} (glob) + 06be89dfe2ae447383f30a2984933352757b6fb4 0 {1334a80b33c3f9873edab728fbbcf500eab61d2e} (*) {'ef1': '0', 'user': 'test'} (glob) + d2fe56e71366c2c5376c89960c281395062c0619 2d8abdb827cdf71ca477ef6985d7ceb257c53c1b 033b3f5ae73db67c10de938fb6f26b949aaef172 0 (*) {'ef1': '13', 'user': 'test'} (glob) $ hg glog - @ changeset: 7:5410a2352fa3 + @ changeset: 7:033b3f5ae73d | tag: tip | user: test | date: Thu Jan 01 00:00:00 1970 +0000 | summary: split2 | - o changeset: 6:a98b35e86cae - | parent: 2:102002290587 + o changeset: 6:2d8abdb827cd + | parent: 2:52149352b372 | user: test | date: Thu Jan 01 00:00:00 1970 +0000 | summary: split1 | - o changeset: 2:102002290587 - | user: test + o changeset: 2:52149352b372 + | user: other-test-user | date: Thu Jan 01 00:00:00 1970 +0000 | summary: add _c | @@ -144,9 +144,9 @@ atop:[10] split1 working directory is now at * (glob) $ hg log -r "desc(_cprim)" -v -p - changeset: 9:719157b217ac + changeset: 9:b434287e665c parent: 1:37445b16603b - user: test + user: other-test-user date: Thu Jan 01 00:00:00 1970 +0000 files: _b _c description: @@ -198,40 +198,40 @@ remaining changes $ hg debugobsolete - 9e84a109b8eb081ad754681ee4b1380d17a3741f aa8f656bb307022172d2648be6fb65322f801225 0 (*) {'ef1': '*', 'user': 'test'} (glob) - f002b57772d7f09b180c407213ae16d92996a988 0 {9e84a109b8eb081ad754681ee4b1380d17a3741f} (*) {'ef1': '*', 'user': 'test'} (glob) - aa8f656bb307022172d2648be6fb65322f801225 a98b35e86cae589b61892127c5ec1c868e41d910 5410a2352fa3114883327beee89e3085eefac25c 0 (*) {'ef1': '*', 'user': 'test'} (glob) - 10200229058723ce8d67f6612c1f6b4f73b1fe73 719157b217acc43d397369a448824ed4c7a302f2 0 (*) {'ef1': '*', 'user': 'test'} (glob) - 5d0c8b0f2d3e5e1ff95f93d7da2ba06650605ab5 0 {10200229058723ce8d67f6612c1f6b4f73b1fe73} (*) {'ef1': '*', 'user': 'test'} (glob) - a98b35e86cae589b61892127c5ec1c868e41d910 286887947725085e03455d79649197feaef1eb9d 0 (*) {'ef1': '*', 'user': 'test'} (glob) - 5410a2352fa3114883327beee89e3085eefac25c 0b67cee46a7f2ad664f994027e7af95b36ae25fe 0 (*) {'ef1': '*', 'user': 'test'} (glob) - 719157b217acc43d397369a448824ed4c7a302f2 ced8fbcce3a7cd33f0e454d2cd63882ce1b6006b 73309fb98db840ba4ec5ad528346dc6ee0b39dcb 0 (*) {'ef1': '*', 'user': 'test'} (glob) + 1334a80b33c3f9873edab728fbbcf500eab61d2e d2fe56e71366c2c5376c89960c281395062c0619 0 (*) {'ef1': '8', 'user': 'test'} (glob) + 06be89dfe2ae447383f30a2984933352757b6fb4 0 {1334a80b33c3f9873edab728fbbcf500eab61d2e} (*) {'ef1': '0', 'user': 'test'} (glob) + d2fe56e71366c2c5376c89960c281395062c0619 2d8abdb827cdf71ca477ef6985d7ceb257c53c1b 033b3f5ae73db67c10de938fb6f26b949aaef172 0 (*) {'ef1': '13', 'user': 'test'} (glob) + 52149352b372d39b19127d5bd2d488b1b63f9f85 b434287e665ce757ee5463a965cb3d119ca9e893 0 (*) {'ef1': '9', 'user': 'test'} (glob) + 7a4fc25a48a5797bb069563854455aecf738d8f2 0 {52149352b372d39b19127d5bd2d488b1b63f9f85} (*) {'ef1': '0', 'user': 'test'} (glob) + 2d8abdb827cdf71ca477ef6985d7ceb257c53c1b e2b4afde39803bd42bb1374b230fca1b1e8cc868 0 (*) {'ef1': '4', 'user': 'test'} (glob) + 033b3f5ae73db67c10de938fb6f26b949aaef172 bb5e4f6020c74e7961a51fda635ea9df9b04dda8 0 (*) {'ef1': '4', 'user': 'test'} (glob) + b434287e665ce757ee5463a965cb3d119ca9e893 ead2066d1dbf14833fe1069df1b735e4e9468c40 1188c4216eba37f18a1de6558564601d00ff2143 0 (*) {'ef1': '13', 'user': 'test'} (glob) $ hg evolve --all move:[10] split1 atop:[13] split4 move:[11] split2 atop:[14] split1 - working directory is now at f200e612ac86 + working directory is now at d74c6715e706 $ hg glog - @ changeset: 15:f200e612ac86 + @ changeset: 15:d74c6715e706 | tag: tip | user: test | date: Thu Jan 01 00:00:00 1970 +0000 | summary: split2 | - o changeset: 14:aec57822a8ff + o changeset: 14:3f134f739075 | user: test | date: Thu Jan 01 00:00:00 1970 +0000 | summary: split1 | - o changeset: 13:73309fb98db8 - | user: test + o changeset: 13:1188c4216eba + | user: other-test-user | date: Thu Jan 01 00:00:00 1970 +0000 | summary: split4 | - o changeset: 12:ced8fbcce3a7 + o changeset: 12:ead2066d1dbf | parent: 1:37445b16603b - | user: test + | user: other-test-user | date: Thu Jan 01 00:00:00 1970 +0000 | summary: split3 | @@ -253,19 +253,19 @@ $ echo "changetofilea" > _a $ hg amend $ hg book - bookA 17:39d16b69c75d - * bookB 17:39d16b69c75d + bookA 17:7a6b35779b85 + * bookB 17:7a6b35779b85 $ hg glog -r "14::" - @ changeset: 17:39d16b69c75d + @ changeset: 17:7a6b35779b85 | bookmark: bookA | bookmark: bookB | tag: tip - | parent: 14:aec57822a8ff + | parent: 14:3f134f739075 | user: test | date: Thu Jan 01 00:00:00 1970 +0000 | summary: split2 | - o changeset: 14:aec57822a8ff + o changeset: 14:3f134f739075 | user: test ~ date: Thu Jan 01 00:00:00 1970 +0000 summary: split1 @@ -297,7 +297,7 @@ created new head Done splitting? [yN] y $ hg glog -r "14::" - @ changeset: 19:a2b5c9d9b362 + @ changeset: 19:60ea019b0f8d | bookmark: bookA | bookmark: bookB | tag: tip @@ -305,20 +305,20 @@ | date: Thu Jan 01 00:00:00 1970 +0000 | summary: split6 | - o changeset: 18:bf3402785e72 - | parent: 14:aec57822a8ff + o changeset: 18:2c7a2e53f23b + | parent: 14:3f134f739075 | user: test | date: Thu Jan 01 00:00:00 1970 +0000 | summary: split5 | - o changeset: 14:aec57822a8ff + o changeset: 14:3f134f739075 | user: test ~ date: Thu Jan 01 00:00:00 1970 +0000 summary: split1 $ hg book - bookA 19:a2b5c9d9b362 - * bookB 19:a2b5c9d9b362 + bookA 19:60ea019b0f8d + * bookB 19:60ea019b0f8d Lastest revision is selected if multiple are given to -r $ hg split -r "desc(_a)::" @@ -337,7 +337,7 @@ > evolutioncommands=split > EOF $ hg split -r "desc(split3)" - abort: cannot split commit: ced8fbcce3a7 not a head + abort: cannot split commit: ead2066d1dbf not a head [255] Changing evolution level to createmarkers @@ -385,3 +385,84 @@ $ hg commit -m "empty" $ hg split 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + +Check that split keeps the right topic + + $ hg up -r tip + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + +Add topic to the hgrc + + $ echo "[extensions]" >> $HGRCPATH + $ echo "topic=$(echo $(dirname $TESTDIR))/hgext3rd/topic/" >> $HGRCPATH + $ hg topic mytopic + $ echo babar > babar + $ echo celeste > celeste + $ hg add babar celeste + $ hg commit -m "Works on mytopic" babar celeste + $ hg summary + parent: 21:615c369f47f0 tip + Works on mytopic + branch: new-branch + commit: 2 unknown (clean) + update: (current) + phases: 9 draft + topic: mytopic + +Split it + + $ hg split << EOF + > Y + > Y + > N + > Y + > Y + > Y + > EOF + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + adding babar + adding celeste + diff --git a/babar b/babar + new file mode 100644 + examine changes to 'babar'? [Ynesfdaq?] Y + + @@ -0,0 +1,1 @@ + +babar + record change 1/2 to 'babar'? [Ynesfdaq?] Y + + diff --git a/celeste b/celeste + new file mode 100644 + examine changes to 'celeste'? [Ynesfdaq?] N + + Done splitting? [yN] Y + diff --git a/celeste b/celeste + new file mode 100644 + examine changes to 'celeste'? [Ynesfdaq?] Y + + @@ -0,0 +1,1 @@ + +celeste + record this change to 'celeste'? [Ynesfdaq?] Y + + no more change to split + +Check that the topic is still here + + $ hg log -r "tip~1::" + changeset: 22:f879ab83f991 + branch: new-branch + topic: mytopic + parent: 20:89e64a2c68b3 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: split7 + + changeset: 23:db75dbc1a3a6 + branch: new-branch + tag: tip + topic: mytopic + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: split8 + + $ hg topic + * mytopic diff -r da0d3d4171cb -r 167567795f6c tests/test-stabilize-conflict.t diff -r da0d3d4171cb -r 167567795f6c tests/test-stabilize-order.t diff -r da0d3d4171cb -r 167567795f6c tests/test-topic-push-concurrent-on.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-topic-push-concurrent-on.t Sun Jul 02 17:28:38 2017 +0200 @@ -0,0 +1,417 @@ +# same as test-topic-push but with the concurrent push feature on + + $ . "$TESTDIR/testlib/topic_setup.sh" + + $ cat << EOF >> $HGRCPATH + > [ui] + > logtemplate = {rev} {branch} {get(namespaces, "topics")} {phase} {desc|firstline}\n + > ssh =python "$RUNTESTDIR/dummyssh" + > [server] + > concurrent-push-mode=check-related + > EOF + + $ hg init main + $ hg init draft + $ cat << EOF >> draft/.hg/hgrc + > [phases] + > publish=False + > EOF + $ hg clone main client + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat << EOF >> client/.hg/hgrc + > [paths] + > draft=../draft + > EOF + + +Testing core behavior to make sure we did not break anything +============================================================ + +Pushing a first changeset + + $ cd client + $ echo aaa > aaa + $ hg add aaa + $ hg commit -m 'CA' + $ hg outgoing -G + comparing with $TESTTMP/main (glob) + searching for changes + @ 0 default draft CA + + $ hg push + pushing to $TESTTMP/main (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + +Pushing two heads + + $ echo aaa > bbb + $ hg add bbb + $ hg commit -m 'CB' + $ echo aaa > ccc + $ hg up 'desc(CA)' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg add ccc + $ hg commit -m 'CC' + created new head + $ hg outgoing -G + comparing with $TESTTMP/main (glob) + searching for changes + @ 2 default draft CC + + o 1 default draft CB + + $ hg push + pushing to $TESTTMP/main (glob) + searching for changes + abort: push creates new remote head 9fe81b7f425d! + (merge or see "hg help push" for details about pushing new heads) + [255] + $ hg outgoing -r 'desc(CB)' -G + comparing with $TESTTMP/main (glob) + searching for changes + o 1 default draft CB + + $ hg push -r 'desc(CB)' + pushing to $TESTTMP/main (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + +Pushing a new branch + + $ hg branch mountain + marked working directory as branch mountain + (branches are permanent and global, did you want a bookmark?) + $ hg commit --amend + $ hg outgoing -G + comparing with $TESTTMP/main (glob) + searching for changes + @ 4 mountain draft CC + + $ hg push + pushing to $TESTTMP/main (glob) + searching for changes + abort: push creates new remote branches: mountain! + (use 'hg push --new-branch' to create new remote branches) + [255] + $ hg push --new-branch + pushing to $TESTTMP/main (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + 2 new obsolescence markers + +Including on non-publishing + + $ hg push --new-branch draft + pushing to $TESTTMP/draft (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 3 changes to 3 files (+1 heads) + 2 new obsolescence markers + +Testing topic behavior +====================== + +Local peer tests +---------------- + + $ hg up -r 'desc(CA)' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg topic babar + $ echo aaa > ddd + $ hg add ddd + $ hg commit -m 'CD' + $ hg log -G # keep track of phase because I saw some strange bug during developement + @ 5 default babar draft CD + | + | o 4 mountain public CC + |/ + | o 1 default public CB + |/ + o 0 default public CA + + +Pushing a new topic to a non publishing server should not be seen as a new head + + $ hg push draft + pushing to $TESTTMP/draft (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + $ hg log -G + @ 5 default babar draft CD + | + | o 4 mountain public CC + |/ + | o 1 default public CB + |/ + o 0 default public CA + + +Pushing a new topic to a publishing server should be seen as a new head + + $ hg push + pushing to $TESTTMP/main (glob) + searching for changes + abort: push creates new remote head 67f579af159d! + (merge or see "hg help push" for details about pushing new heads) + [255] + $ hg log -G + @ 5 default babar draft CD + | + | o 4 mountain public CC + |/ + | o 1 default public CB + |/ + o 0 default public CA + + +wireprotocol tests +------------------ + + $ hg up -r 'desc(CA)' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg topic celeste + $ echo aaa > eee + $ hg add eee + $ hg commit -m 'CE' + $ hg log -G # keep track of phase because I saw some strange bug during developement + @ 6 default celeste draft CE + | + | o 5 default babar draft CD + |/ + | o 4 mountain public CC + |/ + | o 1 default public CB + |/ + o 0 default public CA + + +Pushing a new topic to a non publishing server without topic -> new head + + $ cat << EOF >> ../draft/.hg/hgrc + > [extensions] + > topic=! + > EOF + $ hg push ssh://user@dummy/draft + pushing to ssh://user@dummy/draft + searching for changes + abort: push creates new remote head 84eaf32db6c3! + (merge or see "hg help push" for details about pushing new heads) + [255] + $ hg log -G + @ 6 default celeste draft CE + | + | o 5 default babar draft CD + |/ + | o 4 mountain public CC + |/ + | o 1 default public CB + |/ + o 0 default public CA + + +Pushing a new topic to a non publishing server should not be seen as a new head + + $ printf "topic=" >> ../draft/.hg/hgrc + $ hg config extensions.topic >> ../draft/.hg/hgrc + $ hg push ssh://user@dummy/draft + pushing to ssh://user@dummy/draft + searching for changes + remote: adding changesets + remote: adding manifests + remote: adding file changes + remote: added 1 changesets with 1 changes to 1 files (+1 heads) + $ hg log -G + @ 6 default celeste draft CE + | + | o 5 default babar draft CD + |/ + | o 4 mountain public CC + |/ + | o 1 default public CB + |/ + o 0 default public CA + + +Pushing a new topic to a publishing server should be seen as a new head + + $ hg push ssh://user@dummy/main + pushing to ssh://user@dummy/main + searching for changes + abort: push creates new remote head 67f579af159d! + (merge or see "hg help push" for details about pushing new heads) + [255] + $ hg log -G + @ 6 default celeste draft CE + | + | o 5 default babar draft CD + |/ + | o 4 mountain public CC + |/ + | o 1 default public CB + |/ + o 0 default public CA + + +Check that we reject multiple head on the same topic +---------------------------------------------------- + + $ hg up 'desc(CB)' + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg topic babar + $ echo aaa > fff + $ hg add fff + $ hg commit -m 'CF' + $ hg log -G + @ 7 default babar draft CF + | + | o 6 default celeste draft CE + | | + | | o 5 default babar draft CD + | |/ + | | o 4 mountain public CC + | |/ + o | 1 default public CB + |/ + o 0 default public CA + + + $ hg push draft + pushing to $TESTTMP/draft (glob) + searching for changes + abort: push creates new remote head f0bc62a661be on branch 'default:babar'! + (merge or see "hg help push" for details about pushing new heads) + [255] + +Multiple head on a branch merged in a topic changesets +------------------------------------------------------------------------ + + + $ hg up 'desc(CA)' + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ echo aaa > ggg + $ hg add ggg + $ hg commit -m 'CG' + created new head + $ hg up 'desc(CF)' + switching to topic babar + 2 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg merge 'desc(CG)' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ hg commit -m 'CM' + $ hg log -G + @ 9 default babar draft CM + |\ + | o 8 default draft CG + | | + o | 7 default babar draft CF + | | + | | o 6 default celeste draft CE + | |/ + | | o 5 default babar draft CD + | |/ + | | o 4 mountain public CC + | |/ + o | 1 default public CB + |/ + o 0 default public CA + + +Reject when pushing to draft + + $ hg push draft -r . + pushing to $TESTTMP/draft (glob) + searching for changes + abort: push creates new remote head 4937c4cad39e! + (merge or see "hg help push" for details about pushing new heads) + [255] + + +Reject when pushing to publishing + + $ hg push -r . + pushing to $TESTTMP/main (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 2 changes to 2 files + + $ cd .. + +Test phase move +================================== + +setup, two repo knowns about two small topic branch + + $ hg init repoA + $ hg clone repoA repoB + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat << EOF >> repoA/.hg/hgrc + > [phases] + > publish=False + > EOF + $ cat << EOF >> repoB/.hg/hgrc + > [phases] + > publish=False + > EOF + $ cd repoA + $ echo aaa > base + $ hg add base + $ hg commit -m 'CBASE' + $ echo aaa > aaa + $ hg add aaa + $ hg topic topicA + $ hg commit -m 'CA' + $ hg up 'desc(CBASE)' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ echo aaa > bbb + $ hg add bbb + $ hg topic topicB + $ hg commit -m 'CB' + $ cd .. + $ hg push -R repoA repoB + pushing to repoB + searching for changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 3 changes to 3 files (+1 heads) + $ hg log -G -R repoA + @ 2 default topicB draft CB + | + | o 1 default topicA draft CA + |/ + o 0 default draft CBASE + + +We turn different topic to public on each side, + + $ hg -R repoA phase --public topicA + $ hg -R repoB phase --public topicB + +Pushing should complain because it create to heads on default + + $ hg push -R repoA repoB + pushing to repoB + searching for changes + no changes found + abort: push create a new head on branch "default" + [255] diff -r da0d3d4171cb -r 167567795f6c tests/test-wireproto.t