##// END OF EJS Templates
absorb: preserve changesets which were already empty...
absorb: preserve changesets which were already empty Most commands in Mercurial (commit, rebase, absorb itself) don’t create empty changesets or drop them if they become empty. If there’s a changeset that’s empty, it must be a deliberate choice of the user. At least it shouldn’t be absorb’s responsibility to prune them. The fact that changesets that became empty during absorb are pruned, is unaffected by this. This case was found while writing patches which make it possible to configure absorb and rebase to not drop empty changesets. Even without having such config set, I think it’s valuable to preserve changesets which were already empty.

File last commit:

r45507:6746a710 default
r45518:1ca0047f default
Show More
absorb.py
1137 lines | 40.2 KiB | text/x-python | PythonLexer
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 # absorb.py
#
# Copyright 2016 Facebook, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
"""apply working directory changes to changesets (EXPERIMENTAL)
The absorb extension provides a command to use annotate information to
amend modified chunks into the corresponding non-public changesets.
::
[absorb]
# only check 50 recent non-public changesets at most
David Demelier
absorb: following UI conventions...
r38997 max-stack-size = 50
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 # whether to add noise to new commits to avoid obsolescence cycle
David Demelier
absorb: following UI conventions...
r38997 add-noise = 1
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 # make `amend --correlated` a shortcut to the main command
David Demelier
absorb: following UI conventions...
r38997 amend-flag = correlated
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
[color]
Mark Thomas
absorb: print summary of changesets affected...
r40224 absorb.description = yellow
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 absorb.node = blue bold
absorb.path = bold
"""
Augie Fackler
absorb: note some TODOs from the code review...
r38958 # TODO:
# * Rename config items to [commands] namespace
# * Converge getdraftstack() with other code in core
# * move many attributes on fixupstate to be private
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 from __future__ import absolute_import
import collections
from mercurial.i18n import _
from mercurial import (
cmdutil,
commands,
context,
crecord,
error,
linelog,
mdiff,
node,
obsolete,
patch,
phases,
Augie Fackler
absorb: use pycompat to get xrange...
r38956 pycompat,
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 registrar,
scmutil,
util,
)
Augie Fackler
formatting: blacken the codebase...
r43346 from mercurial.utils import stringutil
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
# be specifying the version(s) of Mercurial they are tested with, or
# leave the attribute unspecified.
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 testedwith = b'ships-with-hg-core'
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
cmdtable = {}
command = registrar.command(cmdtable)
configtable = {}
configitem = registrar.configitem(configtable)
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 configitem(b'absorb', b'add-noise', default=True)
configitem(b'absorb', b'amend-flag', default=None)
configitem(b'absorb', b'max-stack-size', default=50)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
colortable = {
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 b'absorb.description': b'yellow',
b'absorb.node': b'blue bold',
b'absorb.path': b'bold',
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 }
defaultdict = collections.defaultdict
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 class nullui(object):
"""blank ui object doing nothing"""
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 debugflag = False
verbose = False
quiet = True
def __getitem__(name):
def nullfunc(*args, **kwds):
return
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 return nullfunc
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 class emptyfilecontext(object):
"""minimal filecontext representing an empty file"""
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 def data(self):
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 return b''
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
def node(self):
return node.nullid
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 def uniq(lst):
"""list -> list. remove duplicated items without changing the order"""
seen = set()
result = []
for x in lst:
if x not in seen:
seen.add(x)
result.append(x)
return result
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 def getdraftstack(headctx, limit=None):
"""(ctx, int?) -> [ctx]. get a linear stack of non-public changesets.
changesets are sorted in topo order, oldest first.
return at most limit items, if limit is a positive number.
merges are considered as non-draft as well. i.e. every commit
returned has and only has 1 parent.
"""
ctx = headctx
result = []
while ctx.phase() != phases.public:
if limit and len(result) >= limit:
break
parents = ctx.parents()
if len(parents) != 1:
break
result.append(ctx)
ctx = parents[0]
result.reverse()
return result
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
absorb: avoid mutable default arg...
r38954 def getfilestack(stack, path, seenfctxs=None):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 """([ctx], str, set) -> [fctx], {ctx: fctx}
stack is a list of contexts, from old to new. usually they are what
"getdraftstack" returns.
follows renames, but not copies.
seenfctxs is a set of filecontexts that will be considered "immutable".
they are usually what this function returned in earlier calls, useful
to avoid issues that a file was "moved" to multiple places and was then
modified differently, like: "a" was copied to "b", "a" was also copied to
"c" and then "a" was deleted, then both "b" and "c" were "moved" from "a"
and we enforce only one of them to be able to affect "a"'s content.
return an empty list and an empty dict, if the specified path does not
exist in stack[-1] (the top of the stack).
otherwise, return a list of de-duplicated filecontexts, and the map to
convert ctx in the stack to fctx, for possible mutable fctxs. the first item
of the list would be outside the stack and should be considered immutable.
the remaining items are within the stack.
for example, given the following changelog and corresponding filelog
revisions:
changelog: 3----4----5----6----7
filelog: x 0----1----1----2 (x: no such file yet)
- if stack = [5, 6, 7], returns ([0, 1, 2], {5: 1, 6: 1, 7: 2})
- if stack = [3, 4, 5], returns ([e, 0, 1], {4: 0, 5: 1}), where "e" is a
dummy empty filecontext.
- if stack = [2], returns ([], {})
- if stack = [7], returns ([1, 2], {7: 2})
- if stack = [6, 7], returns ([1, 2], {6: 1, 7: 2}), although {6: 1} can be
removed, since 1 is immutable.
"""
Augie Fackler
absorb: avoid mutable default arg...
r38954 if seenfctxs is None:
seenfctxs = set()
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 assert stack
if path not in stack[-1]:
return [], {}
fctxs = []
fctxmap = {}
Augie Fackler
formatting: blacken the codebase...
r43346 pctx = stack[0].p1() # the public (immutable) ctx we stop at
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 for ctx in reversed(stack):
Augie Fackler
formatting: blacken the codebase...
r43346 if path not in ctx: # the file is added in the next commit
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 pctx = ctx
break
fctx = ctx[path]
fctxs.append(fctx)
Augie Fackler
formatting: blacken the codebase...
r43346 if fctx in seenfctxs: # treat fctx as the immutable one
pctx = None # do not add another immutable fctx
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 break
Augie Fackler
formatting: blacken the codebase...
r43346 fctxmap[ctx] = fctx # only for mutable fctxs
Martin von Zweigbergk
absorb: migrate to new method for getting copy info...
r41944 copy = fctx.copysource()
if copy:
Augie Fackler
formatting: blacken the codebase...
r43346 path = copy # follow rename
if path in ctx: # but do not follow copy
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 pctx = ctx.p1()
break
Augie Fackler
formatting: blacken the codebase...
r43346 if pctx is not None: # need an extra immutable fctx
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 if path in pctx:
fctxs.append(pctx[path])
else:
fctxs.append(emptyfilecontext())
fctxs.reverse()
# note: we rely on a property of hg: filerev is not reused for linear
# history. i.e. it's impossible to have:
# changelog: 4----5----6 (linear, no merges)
# filelog: 1----2----1
# ^ reuse filerev (impossible)
# because parents are part of the hash. if that's not true, we need to
# remove uniq and find a different way to identify fctxs.
return uniq(fctxs), fctxmap
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 class overlaystore(patch.filestore):
"""read-only, hybrid store based on a dict and ctx.
memworkingcopy: {path: content}, overrides file contents.
"""
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 def __init__(self, basectx, memworkingcopy):
self.basectx = basectx
self.memworkingcopy = memworkingcopy
def getfile(self, path):
"""comply with mercurial.patch.filestore.getfile"""
if path not in self.basectx:
return None, None, None
fctx = self.basectx[path]
if path in self.memworkingcopy:
content = self.memworkingcopy[path]
else:
content = fctx.data()
mode = (fctx.islink(), fctx.isexec())
Martin von Zweigbergk
absorb: migrate to new method for getting copy info...
r41944 copy = fctx.copysource()
return content, mode, copy
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 def overlaycontext(memworkingcopy, ctx, parents=None, extra=None):
"""({path: content}, ctx, (p1node, p2node)?, {}?) -> memctx
memworkingcopy overrides file contents.
"""
# parents must contain 2 items: (node1, node2)
if parents is None:
parents = ctx.repo().changelog.parents(ctx.node())
if extra is None:
extra = ctx.extra()
date = ctx.date()
desc = ctx.description()
user = ctx.user()
Augie Fackler
absorb: port partway to Python 3...
r39023 files = set(ctx.files()).union(memworkingcopy)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 store = overlaystore(ctx, memworkingcopy)
return context.memctx(
Augie Fackler
formatting: blacken the codebase...
r43346 repo=ctx.repo(),
parents=parents,
text=desc,
files=files,
filectxfn=store,
user=user,
date=date,
branch=None,
extra=extra,
)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
class filefixupstate(object):
"""state needed to apply fixups to a single file
internally, it keeps file contents of several revisions and a linelog.
the linelog uses odd revision numbers for original contents (fctxs passed
to __init__), and even revision numbers for fixups, like:
linelog rev 1: self.fctxs[0] (from an immutable "public" changeset)
linelog rev 2: fixups made to self.fctxs[0]
linelog rev 3: self.fctxs[1] (a child of fctxs[0])
linelog rev 4: fixups made to self.fctxs[1]
...
a typical use is like:
1. call diffwith, to calculate self.fixups
2. (optionally), present self.fixups to the user, or change it
3. call apply, to apply changes
4. read results from "finalcontents", or call getfinalcontent
"""
Mark Thomas
absorb: use a formatter to generate output...
r40223 def __init__(self, fctxs, path, ui=None, opts=None):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 """([fctx], ui or None) -> None
fctxs should be linear, and sorted by topo order - oldest first.
fctxs[0] will be considered as "immutable" and will not be changed.
"""
self.fctxs = fctxs
Mark Thomas
absorb: use a formatter to generate output...
r40223 self.path = path
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 self.ui = ui or nullui()
self.opts = opts or {}
# following fields are built from fctxs. they exist for perf reason
self.contents = [f.data() for f in fctxs]
Augie Fackler
absorb: port partway to Python 3...
r39023 self.contentlines = pycompat.maplist(mdiff.splitnewlines, self.contents)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 self.linelog = self._buildlinelog()
if self.ui.debugflag:
assert self._checkoutlinelog() == self.contents
# following fields will be filled later
Augie Fackler
formatting: blacken the codebase...
r43346 self.chunkstats = [0, 0] # [adopted, total : int]
self.targetlines = [] # [str]
self.fixups = [] # [(linelog rev, a1, a2, b1, b2)]
self.finalcontents = [] # [str]
Mark Thomas
absorb: print summary of changesets affected...
r40224 self.ctxaffected = set()
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
Mark Thomas
absorb: use a formatter to generate output...
r40223 def diffwith(self, targetfctx, fm=None):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 """calculate fixups needed by examining the differences between
self.fctxs[-1] and targetfctx, chunk by chunk.
targetfctx is the target state we move towards. we may or may not be
able to get there because not all modified chunks can be amended into
a non-public fctx unambiguously.
call this only once, before apply().
update self.fixups, self.chunkstats, and self.targetlines.
"""
a = self.contents[-1]
alines = self.contentlines[-1]
b = targetfctx.data()
blines = mdiff.splitnewlines(b)
self.targetlines = blines
self.linelog.annotate(self.linelog.maxrev)
Augie Fackler
formatting: blacken the codebase...
r43346 annotated = self.linelog.annotateresult # [(linelog rev, linenum)]
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 assert len(annotated) == len(alines)
# add a dummy end line to make insertion at the end easier
if annotated:
dummyendline = (annotated[-1][0], annotated[-1][1] + 1)
annotated.append(dummyendline)
# analyse diff blocks
for chunk in self._alldiffchunks(a, b, alines, blines):
newfixups = self._analysediffchunk(chunk, annotated)
Augie Fackler
formatting: blacken the codebase...
r43346 self.chunkstats[0] += bool(newfixups) # 1 or 0
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 self.chunkstats[1] += 1
self.fixups += newfixups
Mark Thomas
absorb: use a formatter to generate output...
r40223 if fm is not None:
self._showchanges(fm, alines, blines, chunk, newfixups)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
def apply(self):
"""apply self.fixups. update self.linelog, self.finalcontents.
call this only once, before getfinalcontent(), after diffwith().
"""
# the following is unnecessary, as it's done by "diffwith":
# self.linelog.annotate(self.linelog.maxrev)
for rev, a1, a2, b1, b2 in reversed(self.fixups):
blines = self.targetlines[b1:b2]
if self.ui.debugflag:
idx = (max(rev - 1, 0)) // 2
Augie Fackler
formatting: blacken the codebase...
r43346 self.ui.write(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 _(b'%s: chunk %d:%d -> %d lines\n')
Augie Fackler
formatting: blacken the codebase...
r43346 % (node.short(self.fctxs[idx].node()), a1, a2, len(blines))
)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 self.linelog.replacelines(rev, a1, a2, b1, b2)
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 if self.opts.get(b'edit_lines', False):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 self.finalcontents = self._checkoutlinelogwithedits()
else:
self.finalcontents = self._checkoutlinelog()
def getfinalcontent(self, fctx):
"""(fctx) -> str. get modified file content for a given filecontext"""
idx = self.fctxs.index(fctx)
return self.finalcontents[idx]
def _analysediffchunk(self, chunk, annotated):
"""analyse a different chunk and return new fixups found
return [] if no lines from the chunk can be safely applied.
the chunk (or lines) cannot be safely applied, if, for example:
- the modified (deleted) lines belong to a public changeset
(self.fctxs[0])
- the chunk is a pure insertion and the adjacent lines (at most 2
lines) belong to different non-public changesets, or do not belong
to any non-public changesets.
- the chunk is modifying lines from different changesets.
in this case, if the number of lines deleted equals to the number
of lines added, assume it's a simple 1:1 map (could be wrong).
otherwise, give up.
- the chunk is modifying lines from a single non-public changeset,
but other revisions touch the area as well. i.e. the lines are
not continuous as seen from the linelog.
"""
a1, a2, b1, b2 = chunk
# find involved indexes from annotate result
involved = annotated[a1:a2]
Augie Fackler
formatting: blacken the codebase...
r43346 if not involved and annotated: # a1 == a2 and a is not empty
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 # pure insertion, check nearby lines. ignore lines belong
# to the public (first) changeset (i.e. annotated[i][0] == 1)
Augie Fackler
absorb: use set literal to avoid intermediate list...
r38955 nearbylinenums = {a2, max(0, a1 - 1)}
Augie Fackler
formatting: blacken the codebase...
r43346 involved = [
annotated[i] for i in nearbylinenums if annotated[i][0] != 1
]
Augie Fackler
cleanup: run pyupgrade on our source tree to clean up varying things...
r44937 involvedrevs = list({r for r, l in involved})
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 newfixups = []
if len(involvedrevs) == 1 and self._iscontinuous(a1, a2 - 1, True):
# chunk belongs to a single revision
rev = involvedrevs[0]
if rev > 1:
fixuprev = rev + 1
newfixups.append((fixuprev, a1, a2, b1, b2))
elif a2 - a1 == b2 - b1 or b1 == b2:
# 1:1 line mapping, or chunk was deleted
Augie Fackler
absorb: use pycompat to get xrange...
r38956 for i in pycompat.xrange(a1, a2):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 rev, linenum = annotated[i]
if rev > 1:
Augie Fackler
formatting: blacken the codebase...
r43346 if b1 == b2: # deletion, simply remove that single line
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 nb1 = nb2 = 0
Augie Fackler
formatting: blacken the codebase...
r43346 else: # 1:1 line mapping, change the corresponding rev
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 nb1 = b1 + i - a1
nb2 = nb1 + 1
fixuprev = rev + 1
newfixups.append((fixuprev, i, i + 1, nb1, nb2))
return self._optimizefixups(newfixups)
@staticmethod
def _alldiffchunks(a, b, alines, blines):
"""like mdiff.allblocks, but only care about differences"""
blocks = mdiff.allblocks(a, b, lines1=alines, lines2=blines)
for chunk, btype in blocks:
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 if btype != b'!':
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 continue
yield chunk
def _buildlinelog(self):
"""calculate the initial linelog based on self.content{,line}s.
this is similar to running a partial "annotate".
"""
llog = linelog.linelog()
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 a, alines = b'', []
Augie Fackler
absorb: use pycompat to get xrange...
r38956 for i in pycompat.xrange(len(self.contents)):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 b, blines = self.contents[i], self.contentlines[i]
llrev = i * 2 + 1
chunks = self._alldiffchunks(a, b, alines, blines)
for a1, a2, b1, b2 in reversed(list(chunks)):
llog.replacelines(llrev, a1, a2, b1, b2)
a, alines = b, blines
return llog
def _checkoutlinelog(self):
"""() -> [str]. check out file contents from linelog"""
contents = []
Augie Fackler
absorb: use pycompat to get xrange...
r38956 for i in pycompat.xrange(len(self.contents)):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 rev = (i + 1) * 2
self.linelog.annotate(rev)
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 content = b''.join(map(self._getline, self.linelog.annotateresult))
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 contents.append(content)
return contents
def _checkoutlinelogwithedits(self):
"""() -> [str]. prompt all lines for edit"""
alllines = self.linelog.getalllines()
# header
Augie Fackler
formatting: blacken the codebase...
r43346 editortext = (
_(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 b'HG: editing %s\nHG: "y" means the line to the right '
b'exists in the changeset to the top\nHG:\n'
Augie Fackler
formatting: blacken the codebase...
r43346 )
% self.fctxs[-1].path()
)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 # [(idx, fctx)]. hide the dummy emptyfilecontext
Augie Fackler
formatting: blacken the codebase...
r43346 visiblefctxs = [
(i, f)
for i, f in enumerate(self.fctxs)
if not isinstance(f, emptyfilecontext)
]
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 for i, (j, f) in enumerate(visiblefctxs):
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 editortext += _(b'HG: %s/%s %s %s\n') % (
b'|' * i,
b'-' * (len(visiblefctxs) - i + 1),
Augie Fackler
formatting: blacken the codebase...
r43346 node.short(f.node()),
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 f.description().split(b'\n', 1)[0],
Augie Fackler
formatting: blacken the codebase...
r43346 )
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 editortext += _(b'HG: %s\n') % (b'|' * len(visiblefctxs))
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 # figure out the lifetime of a line, this is relatively inefficient,
# but probably fine
Augie Fackler
formatting: blacken the codebase...
r43346 lineset = defaultdict(lambda: set()) # {(llrev, linenum): {llrev}}
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 for i, f in visiblefctxs:
self.linelog.annotate((i + 1) * 2)
for l in self.linelog.annotateresult:
lineset[l].add(i)
# append lines
for l in alllines:
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 editortext += b' %s : %s' % (
b''.join(
Augie Fackler
formatting: blacken the codebase...
r43346 [
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 (b'y' if i in lineset[l] else b' ')
Augie Fackler
formatting: blacken the codebase...
r43346 for i, _f in visiblefctxs
]
),
self._getline(l),
)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 # run editor
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 editedtext = self.ui.edit(editortext, b'', action=b'absorb')
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 if not editedtext:
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 raise error.Abort(_(b'empty editor text'))
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 # parse edited result
Matt Harbison
absorb: avoid using a list comprehension to fill a list with fixed values...
r44435 contents = [b''] * len(self.fctxs)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 leftpadpos = 4
colonpos = leftpadpos + len(visiblefctxs) + 1
for l in mdiff.splitnewlines(editedtext):
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 if l.startswith(b'HG:'):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 continue
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 if l[colonpos - 1 : colonpos + 2] != b' : ':
raise error.Abort(_(b'malformed line: %s') % l)
Augie Fackler
formatting: blacken the codebase...
r43346 linecontent = l[colonpos + 2 :]
Augie Fackler
absorb: add a pycompat.bytestr() to fix --edit-lines functionality on Python 3...
r41296 for i, ch in enumerate(
Augie Fackler
formatting: blacken the codebase...
r43346 pycompat.bytestr(l[leftpadpos : colonpos - 1])
):
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 if ch == b'y':
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 contents[visiblefctxs[i][0]] += linecontent
# chunkstats is hard to calculate if anything changes, therefore
# set them to just a simple value (1, 1).
if editedtext != editortext:
self.chunkstats = [1, 1]
return contents
def _getline(self, lineinfo):
"""((rev, linenum)) -> str. convert rev+line number to line content"""
rev, linenum = lineinfo
Augie Fackler
formatting: blacken the codebase...
r43346 if rev & 1: # odd: original line taken from fctxs
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 return self.contentlines[rev // 2][linenum]
Augie Fackler
formatting: blacken the codebase...
r43346 else: # even: fixup line from targetfctx
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 return self.targetlines[linenum]
def _iscontinuous(self, a1, a2, closedinterval=False):
"""(a1, a2 : int) -> bool
check if these lines are continuous. i.e. no other insertions or
deletions (from other revisions) among these lines.
closedinterval decides whether a2 should be included or not. i.e. is
it [a1, a2), or [a1, a2] ?
"""
if a1 >= a2:
return True
llog = self.linelog
offset1 = llog.getoffset(a1)
offset2 = llog.getoffset(a2) + int(closedinterval)
linesinbetween = llog.getalllines(offset1, offset2)
return len(linesinbetween) == a2 - a1 + int(closedinterval)
def _optimizefixups(self, fixups):
"""[(rev, a1, a2, b1, b2)] -> [(rev, a1, a2, b1, b2)].
merge adjacent fixups to make them less fragmented.
"""
result = []
pcurrentchunk = [[-1, -1, -1, -1, -1]]
def pushchunk():
if pcurrentchunk[0][0] != -1:
result.append(tuple(pcurrentchunk[0]))
for i, chunk in enumerate(fixups):
rev, a1, a2, b1, b2 = chunk
lastrev = pcurrentchunk[0][0]
lasta2 = pcurrentchunk[0][2]
lastb2 = pcurrentchunk[0][4]
Augie Fackler
formatting: blacken the codebase...
r43346 if (
a1 == lasta2
and b1 == lastb2
and rev == lastrev
and self._iscontinuous(max(a1 - 1, 0), a1)
):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 # merge into currentchunk
pcurrentchunk[0][2] = a2
pcurrentchunk[0][4] = b2
else:
pushchunk()
pcurrentchunk[0] = list(chunk)
pushchunk()
return result
Mark Thomas
absorb: use a formatter to generate output...
r40223 def _showchanges(self, fm, alines, blines, chunk, fixups):
def trim(line):
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 if line.endswith(b'\n'):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 line = line[:-1]
Mark Thomas
absorb: use a formatter to generate output...
r40223 return line
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
# this is not optimized for perf but _showchanges only gets executed
# with an extra command-line flag.
a1, a2, b1, b2 = chunk
aidxs, bidxs = [0] * (a2 - a1), [0] * (b2 - b1)
for idx, fa1, fa2, fb1, fb2 in fixups:
Augie Fackler
absorb: use pycompat to get xrange...
r38956 for i in pycompat.xrange(fa1, fa2):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 aidxs[i - a1] = (max(idx, 1) - 1) // 2
Augie Fackler
absorb: use pycompat to get xrange...
r38956 for i in pycompat.xrange(fb1, fb2):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 bidxs[i - b1] = (max(idx, 1) - 1) // 2
Mark Thomas
absorb: use a formatter to generate output...
r40223 fm.startitem()
Augie Fackler
formatting: blacken the codebase...
r43346 fm.write(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 b'hunk',
b' %s\n',
b'@@ -%d,%d +%d,%d @@' % (a1, a2 - a1, b1, b2 - b1),
label=b'diff.hunk',
Augie Fackler
formatting: blacken the codebase...
r43346 )
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 fm.data(path=self.path, linetype=b'hunk')
Mark Thomas
absorb: use a formatter to generate output...
r40223
def writeline(idx, diffchar, line, linetype, linelabel):
fm.startitem()
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 node = b''
Mark Thomas
absorb: use a formatter to generate output...
r40223 if idx:
ctx = self.fctxs[idx]
fm.context(fctx=ctx)
node = ctx.hex()
Mark Thomas
absorb: print summary of changesets affected...
r40224 self.ctxaffected.add(ctx.changectx())
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 fm.write(b'node', b'%-7.7s ', node, label=b'absorb.node')
Augie Fackler
formatting: blacken the codebase...
r43346 fm.write(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 b'diffchar ' + linetype,
b'%s%s\n',
Augie Fackler
formatting: blacken the codebase...
r43346 diffchar,
line,
label=linelabel,
)
Mark Thomas
absorb: use a formatter to generate output...
r40223 fm.data(path=self.path, linetype=linetype)
for i in pycompat.xrange(a1, a2):
Augie Fackler
formatting: blacken the codebase...
r43346 writeline(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 aidxs[i - a1],
b'-',
trim(alines[i]),
b'deleted',
b'diff.deleted',
Augie Fackler
formatting: blacken the codebase...
r43346 )
Mark Thomas
absorb: use a formatter to generate output...
r40223 for i in pycompat.xrange(b1, b2):
Augie Fackler
formatting: blacken the codebase...
r43346 writeline(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 bidxs[i - b1],
b'+',
trim(blines[i]),
b'inserted',
b'diff.inserted',
Augie Fackler
formatting: blacken the codebase...
r43346 )
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
class fixupstate(object):
"""state needed to run absorb
internally, it keeps paths and filefixupstates.
a typical use is like filefixupstates:
1. call diffwith, to calculate fixups
2. (optionally), present fixups to the user, or edit fixups
3. call apply, to apply changes to memory
4. call commit, to commit changes to hg database
"""
def __init__(self, stack, ui=None, opts=None):
"""([ctx], ui or None) -> None
stack: should be linear, and sorted by topo order - oldest first.
all commits in stack are considered mutable.
"""
assert stack
self.ui = ui or nullui()
self.opts = opts or {}
self.stack = stack
self.repo = stack[-1].repo().unfiltered()
# following fields will be filled later
Augie Fackler
formatting: blacken the codebase...
r43346 self.paths = [] # [str]
self.status = None # ctx.status output
self.fctxmap = {} # {path: {ctx: fctx}}
self.fixupmap = {} # {path: filefixupstate}
self.replacemap = {} # {oldnode: newnode or None}
self.finalnode = None # head after all fixups
self.ctxaffected = set() # ctx that will be absorbed into
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
Mark Thomas
absorb: use a formatter to generate output...
r40223 def diffwith(self, targetctx, match=None, fm=None):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 """diff and prepare fixups. update self.fixupmap, self.paths"""
# only care about modified files
self.status = self.stack[-1].status(targetctx, match)
self.paths = []
# but if --edit-lines is used, the user may want to edit files
# even if they are not modified
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 editopt = self.opts.get(b'edit_lines')
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 if not self.status.modified and editopt and match:
interestingpaths = match.files()
else:
interestingpaths = self.status.modified
# prepare the filefixupstate
seenfctxs = set()
# sorting is necessary to eliminate ambiguity for the "double move"
# case: "hg cp A B; hg cp A C; hg rm A", then only "B" can affect "A".
for path in sorted(interestingpaths):
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 self.ui.debug(b'calculating fixups for %s\n' % path)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 targetfctx = targetctx[path]
fctxs, ctx2fctx = getfilestack(self.stack, path, seenfctxs)
# ignore symbolic links or binary, or unchanged files
Augie Fackler
formatting: blacken the codebase...
r43346 if any(
f.islink() or stringutil.binary(f.data())
for f in [targetfctx] + fctxs
if not isinstance(f, emptyfilecontext)
):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 continue
if targetfctx.data() == fctxs[-1].data() and not editopt:
continue
seenfctxs.update(fctxs[1:])
self.fctxmap[path] = ctx2fctx
Mark Thomas
absorb: use a formatter to generate output...
r40223 fstate = filefixupstate(fctxs, path, ui=self.ui, opts=self.opts)
if fm is not None:
fm.startitem()
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 fm.plain(b'showing changes for ')
fm.write(b'path', b'%s\n', path, label=b'absorb.path')
fm.data(linetype=b'path')
Mark Thomas
absorb: use a formatter to generate output...
r40223 fstate.diffwith(targetfctx, fm)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 self.fixupmap[path] = fstate
self.paths.append(path)
Mark Thomas
absorb: print summary of changesets affected...
r40224 self.ctxaffected.update(fstate.ctxaffected)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
def apply(self):
"""apply fixups to individual filefixupstates"""
Gregory Szorc
py3: define and use pycompat.iteritems() for hgext/...
r43375 for path, state in pycompat.iteritems(self.fixupmap):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 if self.ui.debugflag:
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 self.ui.write(_(b'applying fixups to %s\n') % path)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 state.apply()
@property
def chunkstats(self):
"""-> {path: chunkstats}. collect chunkstats from filefixupstates"""
Augie Fackler
cleanup: run pyupgrade on our source tree to clean up varying things...
r44937 return {
path: state.chunkstats
Gregory Szorc
py3: define and use pycompat.iteritems() for hgext/...
r43375 for path, state in pycompat.iteritems(self.fixupmap)
Augie Fackler
cleanup: run pyupgrade on our source tree to clean up varying things...
r44937 }
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
def commit(self):
"""commit changes. update self.finalnode, self.replacemap"""
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 with self.repo.transaction(b'absorb') as tr:
Rodrigo Damazio Bovendorp
absorb: aborting if another operation is in progress...
r42297 self._commitstack()
self._movebookmarks(tr)
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 if self.repo[b'.'].node() in self.replacemap:
Rodrigo Damazio Bovendorp
absorb: aborting if another operation is in progress...
r42297 self._moveworkingdirectoryparent()
self._cleanupoldcommits()
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 return self.finalnode
def printchunkstats(self):
"""print things like '1 of 2 chunk(s) applied'"""
ui = self.ui
chunkstats = self.chunkstats
if ui.verbose:
# chunkstats for each file
Gregory Szorc
py3: define and use pycompat.iteritems() for hgext/...
r43375 for path, stat in pycompat.iteritems(chunkstats):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 if stat[0]:
Augie Fackler
formatting: blacken the codebase...
r43346 ui.write(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 _(b'%s: %d of %d chunk(s) applied\n')
Augie Fackler
formatting: blacken the codebase...
r43346 % (path, stat[0], stat[1])
)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 elif not ui.quiet:
# a summary for all files
stats = chunkstats.values()
applied, total = (sum(s[i] for s in stats) for i in (0, 1))
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 ui.write(_(b'%d of %d chunk(s) applied\n') % (applied, total))
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
def _commitstack(self):
"""make new commits. update self.finalnode, self.replacemap.
it is splitted from "commit" to avoid too much indentation.
"""
# last node (20-char) committed by us
lastcommitted = None
# p1 which overrides the parent of the next commit, "None" means use
# the original parent unchanged
nextp1 = None
for ctx in self.stack:
memworkingcopy = self._getnewfilecontents(ctx)
if not memworkingcopy and not lastcommitted:
# nothing changed, nothing commited
nextp1 = ctx
continue
Manuel Jacob
absorb: preserve changesets which were already empty...
r45518 if ctx.files() and self._willbecomenoop(
memworkingcopy, ctx, nextp1
):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 # changeset is no longer necessary
self.replacemap[ctx.node()] = None
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 msg = _(b'became empty and was dropped')
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 else:
# changeset needs re-commit
nodestr = self._commitsingle(memworkingcopy, ctx, p1=nextp1)
lastcommitted = self.repo[nodestr]
nextp1 = lastcommitted
self.replacemap[ctx.node()] = lastcommitted.node()
if memworkingcopy:
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 msg = _(b'%d file(s) changed, became %s') % (
Augie Fackler
formatting: blacken the codebase...
r43346 len(memworkingcopy),
self._ctx2str(lastcommitted),
)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 else:
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 msg = _(b'became %s') % self._ctx2str(lastcommitted)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 if self.ui.verbose and msg:
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 self.ui.write(_(b'%s: %s\n') % (self._ctx2str(ctx), msg))
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 self.finalnode = lastcommitted and lastcommitted.node()
def _ctx2str(self, ctx):
if self.ui.debugflag:
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 return b'%d:%s' % (ctx.rev(), ctx.hex())
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 else:
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 return b'%d:%s' % (ctx.rev(), node.short(ctx.node()))
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
def _getnewfilecontents(self, ctx):
"""(ctx) -> {path: str}
fetch file contents from filefixupstates.
return the working copy overrides - files different from ctx.
"""
result = {}
for path in self.paths:
Augie Fackler
formatting: blacken the codebase...
r43346 ctx2fctx = self.fctxmap[path] # {ctx: fctx}
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 if ctx not in ctx2fctx:
continue
fctx = ctx2fctx[ctx]
content = fctx.data()
newcontent = self.fixupmap[path].getfinalcontent(fctx)
if content != newcontent:
result[fctx.path()] = newcontent
return result
def _movebookmarks(self, tr):
repo = self.repo
Augie Fackler
formatting: blacken the codebase...
r43346 needupdate = [
(name, self.replacemap[hsh])
Gregory Szorc
py3: define and use pycompat.iteritems() for hgext/...
r43375 for name, hsh in pycompat.iteritems(repo._bookmarks)
Augie Fackler
formatting: blacken the codebase...
r43346 if hsh in self.replacemap
]
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 changes = []
for name, hsh in needupdate:
if hsh:
changes.append((name, hsh))
if self.ui.verbose:
Augie Fackler
formatting: blacken the codebase...
r43346 self.ui.write(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 _(b'moving bookmark %s to %s\n') % (name, node.hex(hsh))
Augie Fackler
formatting: blacken the codebase...
r43346 )
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 else:
changes.append((name, None))
if self.ui.verbose:
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 self.ui.write(_(b'deleting bookmark %s\n') % name)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 repo._bookmarks.applychanges(repo, tr, changes)
def _moveworkingdirectoryparent(self):
if not self.finalnode:
# Find the latest not-{obsoleted,stripped} parent.
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 revs = self.repo.revs(b'max(::. - %ln)', self.replacemap.keys())
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 ctx = self.repo[revs.first()]
self.finalnode = ctx.node()
else:
ctx = self.repo[self.finalnode]
dirstate = self.repo.dirstate
# dirstate.rebuild invalidates fsmonitorstate, causing "hg status" to
# be slow. in absorb's case, no need to invalidate fsmonitorstate.
noop = lambda: 0
restore = noop
Martin von Zweigbergk
py3: delete b'' prefix from safehasattr arguments...
r43385 if util.safehasattr(dirstate, '_fsmonitorstate'):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 bak = dirstate._fsmonitorstate.invalidate
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 def restore():
dirstate._fsmonitorstate.invalidate = bak
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 dirstate._fsmonitorstate.invalidate = noop
try:
with dirstate.parentchange():
dirstate.rebuild(ctx.node(), ctx.manifest(), self.paths)
finally:
restore()
@staticmethod
def _willbecomenoop(memworkingcopy, ctx, pctx=None):
"""({path: content}, ctx, ctx) -> bool. test if a commit will be noop
if it will become an empty commit (does not change anything, after the
memworkingcopy overrides), return True. otherwise return False.
"""
if not pctx:
parents = ctx.parents()
if len(parents) != 1:
return False
pctx = parents[0]
Manuel Jacob
absorb: preserve branch-changing changesets even if empty...
r45516 if ctx.branch() != pctx.branch():
return False
Manuel Jacob
absorb: preserve branch-closing changesets even if empty...
r45517 if ctx.extra().get(b'close'):
return False
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 # ctx changes more files (not a subset of memworkingcopy)
Augie Fackler
absorb: port partway to Python 3...
r39023 if not set(ctx.files()).issubset(set(memworkingcopy)):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 return False
Gregory Szorc
py3: define and use pycompat.iteritems() for hgext/...
r43375 for path, content in pycompat.iteritems(memworkingcopy):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 if path not in pctx or path not in ctx:
return False
fctx = ctx[path]
pfctx = pctx[path]
if pfctx.flags() != fctx.flags():
return False
if pfctx.data() != content:
return False
return True
def _commitsingle(self, memworkingcopy, ctx, p1=None):
"""(ctx, {path: content}, node) -> node. make a single commit
the commit is a clone from ctx, with a (optionally) different p1, and
different file contents replaced by memworkingcopy.
"""
parents = p1 and (p1, node.nullid)
extra = ctx.extra()
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 if self._useobsolete and self.ui.configbool(b'absorb', b'add-noise'):
extra[b'absorb_source'] = ctx.hex()
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 mctx = overlaycontext(memworkingcopy, ctx, parents, extra=extra)
Martin von Zweigbergk
absorb: let scmutil.cleanupnodes() take care of setting phase...
r41979 return mctx.commit()
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
@util.propertycache
def _useobsolete(self):
"""() -> bool"""
return obsolete.isenabled(self.repo, obsolete.createmarkersopt)
Martin von Zweigbergk
absorb: use scmutil.cleanupnodes() also when obsmarkers are disabled...
r41978 def _cleanupoldcommits(self):
Augie Fackler
formatting: blacken the codebase...
r43346 replacements = {
k: ([v] if v is not None else [])
Gregory Szorc
py3: define and use pycompat.iteritems() for hgext/...
r43375 for k, v in pycompat.iteritems(self.replacemap)
Augie Fackler
formatting: blacken the codebase...
r43346 }
Martin von Zweigbergk
absorb: use scmutil.cleanupnodes() so operation gets set...
r41977 if replacements:
Augie Fackler
formatting: blacken the codebase...
r43346 scmutil.cleanupnodes(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 self.repo, replacements, operation=b'absorb', fixphase=True
Augie Fackler
formatting: blacken the codebase...
r43346 )
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
def _parsechunk(hunk):
"""(crecord.uihunk or patch.recordhunk) -> (path, (a1, a2, [bline]))"""
if type(hunk) not in (crecord.uihunk, patch.recordhunk):
return None, None
path = hunk.header.filename()
a1 = hunk.fromline + len(hunk.before) - 1
# remove before and after context
hunk.before = hunk.after = []
buf = util.stringio()
hunk.write(buf)
patchlines = mdiff.splitnewlines(buf.getvalue())
# hunk.prettystr() will update hunk.removed
a2 = a1 + hunk.removed
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 blines = [l[1:] for l in patchlines[1:] if not l.startswith(b'-')]
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 return path, (a1, a2, blines)
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 def overlaydiffcontext(ctx, chunks):
"""(ctx, [crecord.uihunk]) -> memctx
return a memctx with some [1] patches (chunks) applied to ctx.
[1]: modifications are handled. renames, mode changes, etc. are ignored.
"""
# sadly the applying-patch logic is hardly reusable, and messy:
# 1. the core logic "_applydiff" is too heavy - it writes .rej files, it
# needs a file stream of a patch and will re-parse it, while we have
# structured hunk objects at hand.
# 2. a lot of different implementations about "chunk" (patch.hunk,
# patch.recordhunk, crecord.uihunk)
# as we only care about applying changes to modified files, no mode
# change, no binary diff, and no renames, it's probably okay to
# re-invent the logic using much simpler code here.
Augie Fackler
formatting: blacken the codebase...
r43346 memworkingcopy = {} # {path: content}
patchmap = defaultdict(lambda: []) # {path: [(a1, a2, [bline])]}
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 for path, info in map(_parsechunk, chunks):
if not path or not info:
continue
patchmap[path].append(info)
Gregory Szorc
py3: define and use pycompat.iteritems() for hgext/...
r43375 for path, patches in pycompat.iteritems(patchmap):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 if path not in ctx or not patches:
continue
patches.sort(reverse=True)
lines = mdiff.splitnewlines(ctx[path].data())
for a1, a2, blines in patches:
lines[a1:a2] = blines
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 memworkingcopy[path] = b''.join(lines)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 return overlaycontext(memworkingcopy, ctx)
Augie Fackler
formatting: blacken the codebase...
r43346
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 def absorb(ui, repo, stack=None, targetctx=None, pats=None, opts=None):
"""pick fixup chunks from targetctx, apply them to stack.
if targetctx is None, the working copy context will be used.
if stack is None, the current draft stack will be used.
return fixupstate.
"""
if stack is None:
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 limit = ui.configint(b'absorb', b'max-stack-size')
headctx = repo[b'.']
Martin von Zweigbergk
absorb: be more specific when erroring out on merge commit...
r42452 if len(headctx.parents()) > 1:
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 raise error.Abort(_(b'cannot absorb into a merge'))
Martin von Zweigbergk
absorb: be more specific when erroring out on merge commit...
r42452 stack = getdraftstack(headctx, limit)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 if limit and len(stack) >= limit:
Augie Fackler
formatting: blacken the codebase...
r43346 ui.warn(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 _(
b'absorb: only the recent %d changesets will '
b'be analysed\n'
)
Augie Fackler
formatting: blacken the codebase...
r43346 % limit
)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 if not stack:
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 raise error.Abort(_(b'no mutable changeset to change'))
Augie Fackler
formatting: blacken the codebase...
r43346 if targetctx is None: # default to working copy
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 targetctx = repo[None]
if pats is None:
pats = ()
if opts is None:
opts = {}
state = fixupstate(stack, ui=ui, opts=opts)
matcher = scmutil.match(targetctx, pats, opts)
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 if opts.get(b'interactive'):
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 diff = patch.diff(repo, stack[-1].node(), targetctx.node(), matcher)
origchunks = patch.parsepatch(diff)
Augie Fackler
absorb: fix interactive mode I didn't know existed...
r42543 chunks = cmdutil.recordfilter(ui, origchunks, matcher)[0]
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 targetctx = overlaydiffcontext(stack[-1], chunks)
Mark Thomas
absorb: use a formatter to generate output...
r40223 fm = None
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 if opts.get(b'print_changes') or not opts.get(b'apply_changes'):
fm = ui.formatter(b'absorb', opts)
Mark Thomas
absorb: use a formatter to generate output...
r40223 state.diffwith(targetctx, matcher, fm)
if fm is not None:
Mark Thomas
absorb: print summary of changesets affected...
r40224 fm.startitem()
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 fm.write(
b"count", b"\n%d changesets affected\n", len(state.ctxaffected)
)
fm.data(linetype=b'summary')
Mark Thomas
absorb: print summary of changesets affected...
r40224 for ctx in reversed(stack):
if ctx not in state.ctxaffected:
continue
fm.startitem()
fm.context(ctx=ctx)
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 fm.data(linetype=b'changeset')
fm.write(b'node', b'%-7.7s ', ctx.hex(), label=b'absorb.node')
Mark Thomas
absorb: print summary of changesets affected...
r40224 descfirstline = ctx.description().splitlines()[0]
Augie Fackler
formatting: blacken the codebase...
r43346 fm.write(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 b'descfirstline',
b'%s\n',
Augie Fackler
formatting: blacken the codebase...
r43346 descfirstline,
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 label=b'absorb.description',
Augie Fackler
formatting: blacken the codebase...
r43346 )
Mark Thomas
absorb: use a formatter to generate output...
r40223 fm.end()
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 if not opts.get(b'dry_run'):
Augie Fackler
formatting: blacken the codebase...
r43346 if (
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 not opts.get(b'apply_changes')
Augie Fackler
formatting: blacken the codebase...
r43346 and state.ctxaffected
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 and ui.promptchoice(
b"apply changes (yn)? $$ &Yes $$ &No", default=1
)
Augie Fackler
formatting: blacken the codebase...
r43346 ):
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 raise error.Abort(_(b'absorb cancelled\n'))
Mark Thomas
absorb: prompt user to accept absorb changes by default...
r40226
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 state.apply()
if state.commit():
state.printchunkstats()
elif not ui.quiet:
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 ui.write(_(b'nothing applied\n'))
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 return state
Augie Fackler
formatting: blacken the codebase...
r43346
@command(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 b'absorb',
Augie Fackler
formatting: blacken the codebase...
r43346 [
(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 b'a',
b'apply-changes',
Augie Fackler
formatting: blacken the codebase...
r43346 None,
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 _(b'apply changes without prompting for confirmation'),
Augie Fackler
formatting: blacken the codebase...
r43346 ),
(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 b'p',
b'print-changes',
Augie Fackler
formatting: blacken the codebase...
r43346 None,
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 _(b'always print which changesets are modified by which changes'),
Augie Fackler
formatting: blacken the codebase...
r43346 ),
(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 b'i',
b'interactive',
Augie Fackler
formatting: blacken the codebase...
r43346 None,
Martin von Zweigbergk
absorb: graduate -i flag from experimental...
r44713 _(b'interactively select which chunks to apply'),
Augie Fackler
formatting: blacken the codebase...
r43346 ),
(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 b'e',
b'edit-lines',
Augie Fackler
formatting: blacken the codebase...
r43346 None,
_(
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 b'edit what lines belong to which changesets before commit '
b'(EXPERIMENTAL)'
Augie Fackler
formatting: blacken the codebase...
r43346 ),
),
]
+ commands.dryrunopts
+ commands.templateopts
+ commands.walkopts,
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 _(b'hg absorb [OPTION] [FILE]...'),
Augie Fackler
formatting: blacken the codebase...
r43346 helpcategory=command.CATEGORY_COMMITTING,
helpbasic=True,
)
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953 def absorbcmd(ui, repo, *pats, **opts):
"""incorporate corrections into the stack of draft changesets
absorb analyzes each change in your working directory and attempts to
amend the changed lines into the changesets in your stack that first
introduced those lines.
If absorb cannot find an unambiguous changeset to amend for a change,
that change will be left in the working directory, untouched. They can be
observed by :hg:`status` or :hg:`diff` afterwards. In other words,
absorb does not write to the working directory.
Changesets outside the revset `::. and not public() and not merge()` will
not be changed.
Changesets that become empty after applying the changes will be deleted.
Mark Thomas
absorb: update help text...
r40246 By default, absorb will show what it plans to do and prompt for
confirmation. If you are confident that the changes will be absorbed
to the correct place, run :hg:`absorb -a` to apply the changes
immediately.
Augie Fackler
absorb: import extension from Facebook's hg-experimental...
r38953
Returns 0 on success, 1 if all chunks were ignored and nothing amended.
"""
Pulkit Goyal
py3: fix kwargs handling in hgext/absorb.py...
r39822 opts = pycompat.byteskwargs(opts)
Rodrigo Damazio Bovendorp
absorb: aborting if another operation is in progress...
r42297
with repo.wlock(), repo.lock():
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 if not opts[b'dry_run']:
Rodrigo Damazio Bovendorp
absorb: aborting if another operation is in progress...
r42297 cmdutil.checkunfinished(repo)
state = absorb(ui, repo, pats=pats, opts=opts)
if sum(s[0] for s in state.chunkstats.values()) == 0:
return 1