comparison hgext/absorb.py @ 43076:2372284d9457

formatting: blacken the codebase This is using my patch to black (https://github.com/psf/black/pull/826) so we don't un-wrap collection literals. Done with: hg files 'set:**.py - mercurial/thirdparty/** - "contrib/python-zstandard/**"' | xargs black -S # skip-blame mass-reformatting only # no-check-commit reformats foo_bar functions Differential Revision: https://phab.mercurial-scm.org/D6971
author Augie Fackler <augie@google.com>
date Sun, 06 Oct 2019 09:45:02 -0400
parents c1bf63ac30c5
children 687b865b95ad
comparison
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
51 pycompat, 51 pycompat,
52 registrar, 52 registrar,
53 scmutil, 53 scmutil,
54 util, 54 util,
55 ) 55 )
56 from mercurial.utils import ( 56 from mercurial.utils import stringutil
57 stringutil,
58 )
59 57
60 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for 58 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
61 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should 59 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
62 # be specifying the version(s) of Mercurial they are tested with, or 60 # be specifying the version(s) of Mercurial they are tested with, or
63 # leave the attribute unspecified. 61 # leave the attribute unspecified.
79 'absorb.path': 'bold', 77 'absorb.path': 'bold',
80 } 78 }
81 79
82 defaultdict = collections.defaultdict 80 defaultdict = collections.defaultdict
83 81
82
84 class nullui(object): 83 class nullui(object):
85 """blank ui object doing nothing""" 84 """blank ui object doing nothing"""
85
86 debugflag = False 86 debugflag = False
87 verbose = False 87 verbose = False
88 quiet = True 88 quiet = True
89 89
90 def __getitem__(name): 90 def __getitem__(name):
91 def nullfunc(*args, **kwds): 91 def nullfunc(*args, **kwds):
92 return 92 return
93
93 return nullfunc 94 return nullfunc
95
94 96
95 class emptyfilecontext(object): 97 class emptyfilecontext(object):
96 """minimal filecontext representing an empty file""" 98 """minimal filecontext representing an empty file"""
99
97 def data(self): 100 def data(self):
98 return '' 101 return ''
99 102
100 def node(self): 103 def node(self):
101 return node.nullid 104 return node.nullid
105
102 106
103 def uniq(lst): 107 def uniq(lst):
104 """list -> list. remove duplicated items without changing the order""" 108 """list -> list. remove duplicated items without changing the order"""
105 seen = set() 109 seen = set()
106 result = [] 110 result = []
107 for x in lst: 111 for x in lst:
108 if x not in seen: 112 if x not in seen:
109 seen.add(x) 113 seen.add(x)
110 result.append(x) 114 result.append(x)
111 return result 115 return result
116
112 117
113 def getdraftstack(headctx, limit=None): 118 def getdraftstack(headctx, limit=None):
114 """(ctx, int?) -> [ctx]. get a linear stack of non-public changesets. 119 """(ctx, int?) -> [ctx]. get a linear stack of non-public changesets.
115 120
116 changesets are sorted in topo order, oldest first. 121 changesets are sorted in topo order, oldest first.
130 result.append(ctx) 135 result.append(ctx)
131 ctx = parents[0] 136 ctx = parents[0]
132 result.reverse() 137 result.reverse()
133 return result 138 return result
134 139
140
135 def getfilestack(stack, path, seenfctxs=None): 141 def getfilestack(stack, path, seenfctxs=None):
136 """([ctx], str, set) -> [fctx], {ctx: fctx} 142 """([ctx], str, set) -> [fctx], {ctx: fctx}
137 143
138 stack is a list of contexts, from old to new. usually they are what 144 stack is a list of contexts, from old to new. usually they are what
139 "getdraftstack" returns. 145 "getdraftstack" returns.
177 return [], {} 183 return [], {}
178 184
179 fctxs = [] 185 fctxs = []
180 fctxmap = {} 186 fctxmap = {}
181 187
182 pctx = stack[0].p1() # the public (immutable) ctx we stop at 188 pctx = stack[0].p1() # the public (immutable) ctx we stop at
183 for ctx in reversed(stack): 189 for ctx in reversed(stack):
184 if path not in ctx: # the file is added in the next commit 190 if path not in ctx: # the file is added in the next commit
185 pctx = ctx 191 pctx = ctx
186 break 192 break
187 fctx = ctx[path] 193 fctx = ctx[path]
188 fctxs.append(fctx) 194 fctxs.append(fctx)
189 if fctx in seenfctxs: # treat fctx as the immutable one 195 if fctx in seenfctxs: # treat fctx as the immutable one
190 pctx = None # do not add another immutable fctx 196 pctx = None # do not add another immutable fctx
191 break 197 break
192 fctxmap[ctx] = fctx # only for mutable fctxs 198 fctxmap[ctx] = fctx # only for mutable fctxs
193 copy = fctx.copysource() 199 copy = fctx.copysource()
194 if copy: 200 if copy:
195 path = copy # follow rename 201 path = copy # follow rename
196 if path in ctx: # but do not follow copy 202 if path in ctx: # but do not follow copy
197 pctx = ctx.p1() 203 pctx = ctx.p1()
198 break 204 break
199 205
200 if pctx is not None: # need an extra immutable fctx 206 if pctx is not None: # need an extra immutable fctx
201 if path in pctx: 207 if path in pctx:
202 fctxs.append(pctx[path]) 208 fctxs.append(pctx[path])
203 else: 209 else:
204 fctxs.append(emptyfilecontext()) 210 fctxs.append(emptyfilecontext())
205 211
211 # ^ reuse filerev (impossible) 217 # ^ reuse filerev (impossible)
212 # because parents are part of the hash. if that's not true, we need to 218 # because parents are part of the hash. if that's not true, we need to
213 # remove uniq and find a different way to identify fctxs. 219 # remove uniq and find a different way to identify fctxs.
214 return uniq(fctxs), fctxmap 220 return uniq(fctxs), fctxmap
215 221
222
216 class overlaystore(patch.filestore): 223 class overlaystore(patch.filestore):
217 """read-only, hybrid store based on a dict and ctx. 224 """read-only, hybrid store based on a dict and ctx.
218 memworkingcopy: {path: content}, overrides file contents. 225 memworkingcopy: {path: content}, overrides file contents.
219 """ 226 """
227
220 def __init__(self, basectx, memworkingcopy): 228 def __init__(self, basectx, memworkingcopy):
221 self.basectx = basectx 229 self.basectx = basectx
222 self.memworkingcopy = memworkingcopy 230 self.memworkingcopy = memworkingcopy
223 231
224 def getfile(self, path): 232 def getfile(self, path):
231 else: 239 else:
232 content = fctx.data() 240 content = fctx.data()
233 mode = (fctx.islink(), fctx.isexec()) 241 mode = (fctx.islink(), fctx.isexec())
234 copy = fctx.copysource() 242 copy = fctx.copysource()
235 return content, mode, copy 243 return content, mode, copy
244
236 245
237 def overlaycontext(memworkingcopy, ctx, parents=None, extra=None): 246 def overlaycontext(memworkingcopy, ctx, parents=None, extra=None):
238 """({path: content}, ctx, (p1node, p2node)?, {}?) -> memctx 247 """({path: content}, ctx, (p1node, p2node)?, {}?) -> memctx
239 memworkingcopy overrides file contents. 248 memworkingcopy overrides file contents.
240 """ 249 """
247 desc = ctx.description() 256 desc = ctx.description()
248 user = ctx.user() 257 user = ctx.user()
249 files = set(ctx.files()).union(memworkingcopy) 258 files = set(ctx.files()).union(memworkingcopy)
250 store = overlaystore(ctx, memworkingcopy) 259 store = overlaystore(ctx, memworkingcopy)
251 return context.memctx( 260 return context.memctx(
252 repo=ctx.repo(), parents=parents, text=desc, 261 repo=ctx.repo(),
253 files=files, filectxfn=store, user=user, date=date, 262 parents=parents,
254 branch=None, extra=extra) 263 text=desc,
264 files=files,
265 filectxfn=store,
266 user=user,
267 date=date,
268 branch=None,
269 extra=extra,
270 )
271
255 272
256 class filefixupstate(object): 273 class filefixupstate(object):
257 """state needed to apply fixups to a single file 274 """state needed to apply fixups to a single file
258 275
259 internally, it keeps file contents of several revisions and a linelog. 276 internally, it keeps file contents of several revisions and a linelog.
292 self.linelog = self._buildlinelog() 309 self.linelog = self._buildlinelog()
293 if self.ui.debugflag: 310 if self.ui.debugflag:
294 assert self._checkoutlinelog() == self.contents 311 assert self._checkoutlinelog() == self.contents
295 312
296 # following fields will be filled later 313 # following fields will be filled later
297 self.chunkstats = [0, 0] # [adopted, total : int] 314 self.chunkstats = [0, 0] # [adopted, total : int]
298 self.targetlines = [] # [str] 315 self.targetlines = [] # [str]
299 self.fixups = [] # [(linelog rev, a1, a2, b1, b2)] 316 self.fixups = [] # [(linelog rev, a1, a2, b1, b2)]
300 self.finalcontents = [] # [str] 317 self.finalcontents = [] # [str]
301 self.ctxaffected = set() 318 self.ctxaffected = set()
302 319
303 def diffwith(self, targetfctx, fm=None): 320 def diffwith(self, targetfctx, fm=None):
304 """calculate fixups needed by examining the differences between 321 """calculate fixups needed by examining the differences between
305 self.fctxs[-1] and targetfctx, chunk by chunk. 322 self.fctxs[-1] and targetfctx, chunk by chunk.
317 b = targetfctx.data() 334 b = targetfctx.data()
318 blines = mdiff.splitnewlines(b) 335 blines = mdiff.splitnewlines(b)
319 self.targetlines = blines 336 self.targetlines = blines
320 337
321 self.linelog.annotate(self.linelog.maxrev) 338 self.linelog.annotate(self.linelog.maxrev)
322 annotated = self.linelog.annotateresult # [(linelog rev, linenum)] 339 annotated = self.linelog.annotateresult # [(linelog rev, linenum)]
323 assert len(annotated) == len(alines) 340 assert len(annotated) == len(alines)
324 # add a dummy end line to make insertion at the end easier 341 # add a dummy end line to make insertion at the end easier
325 if annotated: 342 if annotated:
326 dummyendline = (annotated[-1][0], annotated[-1][1] + 1) 343 dummyendline = (annotated[-1][0], annotated[-1][1] + 1)
327 annotated.append(dummyendline) 344 annotated.append(dummyendline)
328 345
329 # analyse diff blocks 346 # analyse diff blocks
330 for chunk in self._alldiffchunks(a, b, alines, blines): 347 for chunk in self._alldiffchunks(a, b, alines, blines):
331 newfixups = self._analysediffchunk(chunk, annotated) 348 newfixups = self._analysediffchunk(chunk, annotated)
332 self.chunkstats[0] += bool(newfixups) # 1 or 0 349 self.chunkstats[0] += bool(newfixups) # 1 or 0
333 self.chunkstats[1] += 1 350 self.chunkstats[1] += 1
334 self.fixups += newfixups 351 self.fixups += newfixups
335 if fm is not None: 352 if fm is not None:
336 self._showchanges(fm, alines, blines, chunk, newfixups) 353 self._showchanges(fm, alines, blines, chunk, newfixups)
337 354
344 # self.linelog.annotate(self.linelog.maxrev) 361 # self.linelog.annotate(self.linelog.maxrev)
345 for rev, a1, a2, b1, b2 in reversed(self.fixups): 362 for rev, a1, a2, b1, b2 in reversed(self.fixups):
346 blines = self.targetlines[b1:b2] 363 blines = self.targetlines[b1:b2]
347 if self.ui.debugflag: 364 if self.ui.debugflag:
348 idx = (max(rev - 1, 0)) // 2 365 idx = (max(rev - 1, 0)) // 2
349 self.ui.write(_('%s: chunk %d:%d -> %d lines\n') 366 self.ui.write(
350 % (node.short(self.fctxs[idx].node()), 367 _('%s: chunk %d:%d -> %d lines\n')
351 a1, a2, len(blines))) 368 % (node.short(self.fctxs[idx].node()), a1, a2, len(blines))
369 )
352 self.linelog.replacelines(rev, a1, a2, b1, b2) 370 self.linelog.replacelines(rev, a1, a2, b1, b2)
353 if self.opts.get('edit_lines', False): 371 if self.opts.get('edit_lines', False):
354 self.finalcontents = self._checkoutlinelogwithedits() 372 self.finalcontents = self._checkoutlinelogwithedits()
355 else: 373 else:
356 self.finalcontents = self._checkoutlinelog() 374 self.finalcontents = self._checkoutlinelog()
380 not continuous as seen from the linelog. 398 not continuous as seen from the linelog.
381 """ 399 """
382 a1, a2, b1, b2 = chunk 400 a1, a2, b1, b2 = chunk
383 # find involved indexes from annotate result 401 # find involved indexes from annotate result
384 involved = annotated[a1:a2] 402 involved = annotated[a1:a2]
385 if not involved and annotated: # a1 == a2 and a is not empty 403 if not involved and annotated: # a1 == a2 and a is not empty
386 # pure insertion, check nearby lines. ignore lines belong 404 # pure insertion, check nearby lines. ignore lines belong
387 # to the public (first) changeset (i.e. annotated[i][0] == 1) 405 # to the public (first) changeset (i.e. annotated[i][0] == 1)
388 nearbylinenums = {a2, max(0, a1 - 1)} 406 nearbylinenums = {a2, max(0, a1 - 1)}
389 involved = [annotated[i] 407 involved = [
390 for i in nearbylinenums if annotated[i][0] != 1] 408 annotated[i] for i in nearbylinenums if annotated[i][0] != 1
409 ]
391 involvedrevs = list(set(r for r, l in involved)) 410 involvedrevs = list(set(r for r, l in involved))
392 newfixups = [] 411 newfixups = []
393 if len(involvedrevs) == 1 and self._iscontinuous(a1, a2 - 1, True): 412 if len(involvedrevs) == 1 and self._iscontinuous(a1, a2 - 1, True):
394 # chunk belongs to a single revision 413 # chunk belongs to a single revision
395 rev = involvedrevs[0] 414 rev = involvedrevs[0]
399 elif a2 - a1 == b2 - b1 or b1 == b2: 418 elif a2 - a1 == b2 - b1 or b1 == b2:
400 # 1:1 line mapping, or chunk was deleted 419 # 1:1 line mapping, or chunk was deleted
401 for i in pycompat.xrange(a1, a2): 420 for i in pycompat.xrange(a1, a2):
402 rev, linenum = annotated[i] 421 rev, linenum = annotated[i]
403 if rev > 1: 422 if rev > 1:
404 if b1 == b2: # deletion, simply remove that single line 423 if b1 == b2: # deletion, simply remove that single line
405 nb1 = nb2 = 0 424 nb1 = nb2 = 0
406 else: # 1:1 line mapping, change the corresponding rev 425 else: # 1:1 line mapping, change the corresponding rev
407 nb1 = b1 + i - a1 426 nb1 = b1 + i - a1
408 nb2 = nb1 + 1 427 nb2 = nb1 + 1
409 fixuprev = rev + 1 428 fixuprev = rev + 1
410 newfixups.append((fixuprev, i, i + 1, nb1, nb2)) 429 newfixups.append((fixuprev, i, i + 1, nb1, nb2))
411 return self._optimizefixups(newfixups) 430 return self._optimizefixups(newfixups)
446 465
447 def _checkoutlinelogwithedits(self): 466 def _checkoutlinelogwithedits(self):
448 """() -> [str]. prompt all lines for edit""" 467 """() -> [str]. prompt all lines for edit"""
449 alllines = self.linelog.getalllines() 468 alllines = self.linelog.getalllines()
450 # header 469 # header
451 editortext = (_('HG: editing %s\nHG: "y" means the line to the right ' 470 editortext = (
452 'exists in the changeset to the top\nHG:\n') 471 _(
453 % self.fctxs[-1].path()) 472 'HG: editing %s\nHG: "y" means the line to the right '
473 'exists in the changeset to the top\nHG:\n'
474 )
475 % self.fctxs[-1].path()
476 )
454 # [(idx, fctx)]. hide the dummy emptyfilecontext 477 # [(idx, fctx)]. hide the dummy emptyfilecontext
455 visiblefctxs = [(i, f) 478 visiblefctxs = [
456 for i, f in enumerate(self.fctxs) 479 (i, f)
457 if not isinstance(f, emptyfilecontext)] 480 for i, f in enumerate(self.fctxs)
481 if not isinstance(f, emptyfilecontext)
482 ]
458 for i, (j, f) in enumerate(visiblefctxs): 483 for i, (j, f) in enumerate(visiblefctxs):
459 editortext += (_('HG: %s/%s %s %s\n') % 484 editortext += _('HG: %s/%s %s %s\n') % (
460 ('|' * i, '-' * (len(visiblefctxs) - i + 1), 485 '|' * i,
461 node.short(f.node()), 486 '-' * (len(visiblefctxs) - i + 1),
462 f.description().split('\n',1)[0])) 487 node.short(f.node()),
488 f.description().split('\n', 1)[0],
489 )
463 editortext += _('HG: %s\n') % ('|' * len(visiblefctxs)) 490 editortext += _('HG: %s\n') % ('|' * len(visiblefctxs))
464 # figure out the lifetime of a line, this is relatively inefficient, 491 # figure out the lifetime of a line, this is relatively inefficient,
465 # but probably fine 492 # but probably fine
466 lineset = defaultdict(lambda: set()) # {(llrev, linenum): {llrev}} 493 lineset = defaultdict(lambda: set()) # {(llrev, linenum): {llrev}}
467 for i, f in visiblefctxs: 494 for i, f in visiblefctxs:
468 self.linelog.annotate((i + 1) * 2) 495 self.linelog.annotate((i + 1) * 2)
469 for l in self.linelog.annotateresult: 496 for l in self.linelog.annotateresult:
470 lineset[l].add(i) 497 lineset[l].add(i)
471 # append lines 498 # append lines
472 for l in alllines: 499 for l in alllines:
473 editortext += (' %s : %s' % 500 editortext += ' %s : %s' % (
474 (''.join([('y' if i in lineset[l] else ' ') 501 ''.join(
475 for i, _f in visiblefctxs]), 502 [
476 self._getline(l))) 503 ('y' if i in lineset[l] else ' ')
504 for i, _f in visiblefctxs
505 ]
506 ),
507 self._getline(l),
508 )
477 # run editor 509 # run editor
478 editedtext = self.ui.edit(editortext, '', action='absorb') 510 editedtext = self.ui.edit(editortext, '', action='absorb')
479 if not editedtext: 511 if not editedtext:
480 raise error.Abort(_('empty editor text')) 512 raise error.Abort(_('empty editor text'))
481 # parse edited result 513 # parse edited result
483 leftpadpos = 4 515 leftpadpos = 4
484 colonpos = leftpadpos + len(visiblefctxs) + 1 516 colonpos = leftpadpos + len(visiblefctxs) + 1
485 for l in mdiff.splitnewlines(editedtext): 517 for l in mdiff.splitnewlines(editedtext):
486 if l.startswith('HG:'): 518 if l.startswith('HG:'):
487 continue 519 continue
488 if l[colonpos - 1:colonpos + 2] != ' : ': 520 if l[colonpos - 1 : colonpos + 2] != ' : ':
489 raise error.Abort(_('malformed line: %s') % l) 521 raise error.Abort(_('malformed line: %s') % l)
490 linecontent = l[colonpos + 2:] 522 linecontent = l[colonpos + 2 :]
491 for i, ch in enumerate( 523 for i, ch in enumerate(
492 pycompat.bytestr(l[leftpadpos:colonpos - 1])): 524 pycompat.bytestr(l[leftpadpos : colonpos - 1])
525 ):
493 if ch == 'y': 526 if ch == 'y':
494 contents[visiblefctxs[i][0]] += linecontent 527 contents[visiblefctxs[i][0]] += linecontent
495 # chunkstats is hard to calculate if anything changes, therefore 528 # chunkstats is hard to calculate if anything changes, therefore
496 # set them to just a simple value (1, 1). 529 # set them to just a simple value (1, 1).
497 if editedtext != editortext: 530 if editedtext != editortext:
499 return contents 532 return contents
500 533
501 def _getline(self, lineinfo): 534 def _getline(self, lineinfo):
502 """((rev, linenum)) -> str. convert rev+line number to line content""" 535 """((rev, linenum)) -> str. convert rev+line number to line content"""
503 rev, linenum = lineinfo 536 rev, linenum = lineinfo
504 if rev & 1: # odd: original line taken from fctxs 537 if rev & 1: # odd: original line taken from fctxs
505 return self.contentlines[rev // 2][linenum] 538 return self.contentlines[rev // 2][linenum]
506 else: # even: fixup line from targetfctx 539 else: # even: fixup line from targetfctx
507 return self.targetlines[linenum] 540 return self.targetlines[linenum]
508 541
509 def _iscontinuous(self, a1, a2, closedinterval=False): 542 def _iscontinuous(self, a1, a2, closedinterval=False):
510 """(a1, a2 : int) -> bool 543 """(a1, a2 : int) -> bool
511 544
537 for i, chunk in enumerate(fixups): 570 for i, chunk in enumerate(fixups):
538 rev, a1, a2, b1, b2 = chunk 571 rev, a1, a2, b1, b2 = chunk
539 lastrev = pcurrentchunk[0][0] 572 lastrev = pcurrentchunk[0][0]
540 lasta2 = pcurrentchunk[0][2] 573 lasta2 = pcurrentchunk[0][2]
541 lastb2 = pcurrentchunk[0][4] 574 lastb2 = pcurrentchunk[0][4]
542 if (a1 == lasta2 and b1 == lastb2 and rev == lastrev and 575 if (
543 self._iscontinuous(max(a1 - 1, 0), a1)): 576 a1 == lasta2
577 and b1 == lastb2
578 and rev == lastrev
579 and self._iscontinuous(max(a1 - 1, 0), a1)
580 ):
544 # merge into currentchunk 581 # merge into currentchunk
545 pcurrentchunk[0][2] = a2 582 pcurrentchunk[0][2] = a2
546 pcurrentchunk[0][4] = b2 583 pcurrentchunk[0][4] = b2
547 else: 584 else:
548 pushchunk() 585 pushchunk()
549 pcurrentchunk[0] = list(chunk) 586 pcurrentchunk[0] = list(chunk)
550 pushchunk() 587 pushchunk()
551 return result 588 return result
552 589
553 def _showchanges(self, fm, alines, blines, chunk, fixups): 590 def _showchanges(self, fm, alines, blines, chunk, fixups):
554
555 def trim(line): 591 def trim(line):
556 if line.endswith('\n'): 592 if line.endswith('\n'):
557 line = line[:-1] 593 line = line[:-1]
558 return line 594 return line
559 595
566 aidxs[i - a1] = (max(idx, 1) - 1) // 2 602 aidxs[i - a1] = (max(idx, 1) - 1) // 2
567 for i in pycompat.xrange(fb1, fb2): 603 for i in pycompat.xrange(fb1, fb2):
568 bidxs[i - b1] = (max(idx, 1) - 1) // 2 604 bidxs[i - b1] = (max(idx, 1) - 1) // 2
569 605
570 fm.startitem() 606 fm.startitem()
571 fm.write('hunk', ' %s\n', 607 fm.write(
572 '@@ -%d,%d +%d,%d @@' 608 'hunk',
573 % (a1, a2 - a1, b1, b2 - b1), label='diff.hunk') 609 ' %s\n',
610 '@@ -%d,%d +%d,%d @@' % (a1, a2 - a1, b1, b2 - b1),
611 label='diff.hunk',
612 )
574 fm.data(path=self.path, linetype='hunk') 613 fm.data(path=self.path, linetype='hunk')
575 614
576 def writeline(idx, diffchar, line, linetype, linelabel): 615 def writeline(idx, diffchar, line, linetype, linelabel):
577 fm.startitem() 616 fm.startitem()
578 node = '' 617 node = ''
580 ctx = self.fctxs[idx] 619 ctx = self.fctxs[idx]
581 fm.context(fctx=ctx) 620 fm.context(fctx=ctx)
582 node = ctx.hex() 621 node = ctx.hex()
583 self.ctxaffected.add(ctx.changectx()) 622 self.ctxaffected.add(ctx.changectx())
584 fm.write('node', '%-7.7s ', node, label='absorb.node') 623 fm.write('node', '%-7.7s ', node, label='absorb.node')
585 fm.write('diffchar ' + linetype, '%s%s\n', diffchar, line, 624 fm.write(
586 label=linelabel) 625 'diffchar ' + linetype,
626 '%s%s\n',
627 diffchar,
628 line,
629 label=linelabel,
630 )
587 fm.data(path=self.path, linetype=linetype) 631 fm.data(path=self.path, linetype=linetype)
588 632
589 for i in pycompat.xrange(a1, a2): 633 for i in pycompat.xrange(a1, a2):
590 writeline(aidxs[i - a1], '-', trim(alines[i]), 'deleted', 634 writeline(
591 'diff.deleted') 635 aidxs[i - a1], '-', trim(alines[i]), 'deleted', 'diff.deleted'
636 )
592 for i in pycompat.xrange(b1, b2): 637 for i in pycompat.xrange(b1, b2):
593 writeline(bidxs[i - b1], '+', trim(blines[i]), 'inserted', 638 writeline(
594 'diff.inserted') 639 bidxs[i - b1], '+', trim(blines[i]), 'inserted', 'diff.inserted'
640 )
641
595 642
596 class fixupstate(object): 643 class fixupstate(object):
597 """state needed to run absorb 644 """state needed to run absorb
598 645
599 internally, it keeps paths and filefixupstates. 646 internally, it keeps paths and filefixupstates.
617 self.opts = opts or {} 664 self.opts = opts or {}
618 self.stack = stack 665 self.stack = stack
619 self.repo = stack[-1].repo().unfiltered() 666 self.repo = stack[-1].repo().unfiltered()
620 667
621 # following fields will be filled later 668 # following fields will be filled later
622 self.paths = [] # [str] 669 self.paths = [] # [str]
623 self.status = None # ctx.status output 670 self.status = None # ctx.status output
624 self.fctxmap = {} # {path: {ctx: fctx}} 671 self.fctxmap = {} # {path: {ctx: fctx}}
625 self.fixupmap = {} # {path: filefixupstate} 672 self.fixupmap = {} # {path: filefixupstate}
626 self.replacemap = {} # {oldnode: newnode or None} 673 self.replacemap = {} # {oldnode: newnode or None}
627 self.finalnode = None # head after all fixups 674 self.finalnode = None # head after all fixups
628 self.ctxaffected = set() # ctx that will be absorbed into 675 self.ctxaffected = set() # ctx that will be absorbed into
629 676
630 def diffwith(self, targetctx, match=None, fm=None): 677 def diffwith(self, targetctx, match=None, fm=None):
631 """diff and prepare fixups. update self.fixupmap, self.paths""" 678 """diff and prepare fixups. update self.fixupmap, self.paths"""
632 # only care about modified files 679 # only care about modified files
633 self.status = self.stack[-1].status(targetctx, match) 680 self.status = self.stack[-1].status(targetctx, match)
646 for path in sorted(interestingpaths): 693 for path in sorted(interestingpaths):
647 self.ui.debug('calculating fixups for %s\n' % path) 694 self.ui.debug('calculating fixups for %s\n' % path)
648 targetfctx = targetctx[path] 695 targetfctx = targetctx[path]
649 fctxs, ctx2fctx = getfilestack(self.stack, path, seenfctxs) 696 fctxs, ctx2fctx = getfilestack(self.stack, path, seenfctxs)
650 # ignore symbolic links or binary, or unchanged files 697 # ignore symbolic links or binary, or unchanged files
651 if any(f.islink() or stringutil.binary(f.data()) 698 if any(
652 for f in [targetfctx] + fctxs 699 f.islink() or stringutil.binary(f.data())
653 if not isinstance(f, emptyfilecontext)): 700 for f in [targetfctx] + fctxs
701 if not isinstance(f, emptyfilecontext)
702 ):
654 continue 703 continue
655 if targetfctx.data() == fctxs[-1].data() and not editopt: 704 if targetfctx.data() == fctxs[-1].data() and not editopt:
656 continue 705 continue
657 seenfctxs.update(fctxs[1:]) 706 seenfctxs.update(fctxs[1:])
658 self.fctxmap[path] = ctx2fctx 707 self.fctxmap[path] = ctx2fctx
675 state.apply() 724 state.apply()
676 725
677 @property 726 @property
678 def chunkstats(self): 727 def chunkstats(self):
679 """-> {path: chunkstats}. collect chunkstats from filefixupstates""" 728 """-> {path: chunkstats}. collect chunkstats from filefixupstates"""
680 return dict((path, state.chunkstats) 729 return dict(
681 for path, state in self.fixupmap.iteritems()) 730 (path, state.chunkstats)
731 for path, state in self.fixupmap.iteritems()
732 )
682 733
683 def commit(self): 734 def commit(self):
684 """commit changes. update self.finalnode, self.replacemap""" 735 """commit changes. update self.finalnode, self.replacemap"""
685 with self.repo.transaction('absorb') as tr: 736 with self.repo.transaction('absorb') as tr:
686 self._commitstack() 737 self._commitstack()
696 chunkstats = self.chunkstats 747 chunkstats = self.chunkstats
697 if ui.verbose: 748 if ui.verbose:
698 # chunkstats for each file 749 # chunkstats for each file
699 for path, stat in chunkstats.iteritems(): 750 for path, stat in chunkstats.iteritems():
700 if stat[0]: 751 if stat[0]:
701 ui.write(_('%s: %d of %d chunk(s) applied\n') 752 ui.write(
702 % (path, stat[0], stat[1])) 753 _('%s: %d of %d chunk(s) applied\n')
754 % (path, stat[0], stat[1])
755 )
703 elif not ui.quiet: 756 elif not ui.quiet:
704 # a summary for all files 757 # a summary for all files
705 stats = chunkstats.values() 758 stats = chunkstats.values()
706 applied, total = (sum(s[i] for s in stats) for i in (0, 1)) 759 applied, total = (sum(s[i] for s in stats) for i in (0, 1))
707 ui.write(_('%d of %d chunk(s) applied\n') % (applied, total)) 760 ui.write(_('%d of %d chunk(s) applied\n') % (applied, total))
731 lastcommitted = self.repo[nodestr] 784 lastcommitted = self.repo[nodestr]
732 nextp1 = lastcommitted 785 nextp1 = lastcommitted
733 self.replacemap[ctx.node()] = lastcommitted.node() 786 self.replacemap[ctx.node()] = lastcommitted.node()
734 if memworkingcopy: 787 if memworkingcopy:
735 msg = _('%d file(s) changed, became %s') % ( 788 msg = _('%d file(s) changed, became %s') % (
736 len(memworkingcopy), self._ctx2str(lastcommitted)) 789 len(memworkingcopy),
790 self._ctx2str(lastcommitted),
791 )
737 else: 792 else:
738 msg = _('became %s') % self._ctx2str(lastcommitted) 793 msg = _('became %s') % self._ctx2str(lastcommitted)
739 if self.ui.verbose and msg: 794 if self.ui.verbose and msg:
740 self.ui.write(_('%s: %s\n') % (self._ctx2str(ctx), msg)) 795 self.ui.write(_('%s: %s\n') % (self._ctx2str(ctx), msg))
741 self.finalnode = lastcommitted and lastcommitted.node() 796 self.finalnode = lastcommitted and lastcommitted.node()
752 fetch file contents from filefixupstates. 807 fetch file contents from filefixupstates.
753 return the working copy overrides - files different from ctx. 808 return the working copy overrides - files different from ctx.
754 """ 809 """
755 result = {} 810 result = {}
756 for path in self.paths: 811 for path in self.paths:
757 ctx2fctx = self.fctxmap[path] # {ctx: fctx} 812 ctx2fctx = self.fctxmap[path] # {ctx: fctx}
758 if ctx not in ctx2fctx: 813 if ctx not in ctx2fctx:
759 continue 814 continue
760 fctx = ctx2fctx[ctx] 815 fctx = ctx2fctx[ctx]
761 content = fctx.data() 816 content = fctx.data()
762 newcontent = self.fixupmap[path].getfinalcontent(fctx) 817 newcontent = self.fixupmap[path].getfinalcontent(fctx)
764 result[fctx.path()] = newcontent 819 result[fctx.path()] = newcontent
765 return result 820 return result
766 821
767 def _movebookmarks(self, tr): 822 def _movebookmarks(self, tr):
768 repo = self.repo 823 repo = self.repo
769 needupdate = [(name, self.replacemap[hsh]) 824 needupdate = [
770 for name, hsh in repo._bookmarks.iteritems() 825 (name, self.replacemap[hsh])
771 if hsh in self.replacemap] 826 for name, hsh in repo._bookmarks.iteritems()
827 if hsh in self.replacemap
828 ]
772 changes = [] 829 changes = []
773 for name, hsh in needupdate: 830 for name, hsh in needupdate:
774 if hsh: 831 if hsh:
775 changes.append((name, hsh)) 832 changes.append((name, hsh))
776 if self.ui.verbose: 833 if self.ui.verbose:
777 self.ui.write(_('moving bookmark %s to %s\n') 834 self.ui.write(
778 % (name, node.hex(hsh))) 835 _('moving bookmark %s to %s\n') % (name, node.hex(hsh))
836 )
779 else: 837 else:
780 changes.append((name, None)) 838 changes.append((name, None))
781 if self.ui.verbose: 839 if self.ui.verbose:
782 self.ui.write(_('deleting bookmark %s\n') % name) 840 self.ui.write(_('deleting bookmark %s\n') % name)
783 repo._bookmarks.applychanges(repo, tr, changes) 841 repo._bookmarks.applychanges(repo, tr, changes)
796 # be slow. in absorb's case, no need to invalidate fsmonitorstate. 854 # be slow. in absorb's case, no need to invalidate fsmonitorstate.
797 noop = lambda: 0 855 noop = lambda: 0
798 restore = noop 856 restore = noop
799 if util.safehasattr(dirstate, '_fsmonitorstate'): 857 if util.safehasattr(dirstate, '_fsmonitorstate'):
800 bak = dirstate._fsmonitorstate.invalidate 858 bak = dirstate._fsmonitorstate.invalidate
859
801 def restore(): 860 def restore():
802 dirstate._fsmonitorstate.invalidate = bak 861 dirstate._fsmonitorstate.invalidate = bak
862
803 dirstate._fsmonitorstate.invalidate = noop 863 dirstate._fsmonitorstate.invalidate = noop
804 try: 864 try:
805 with dirstate.parentchange(): 865 with dirstate.parentchange():
806 dirstate.rebuild(ctx.node(), ctx.manifest(), self.paths) 866 dirstate.rebuild(ctx.node(), ctx.manifest(), self.paths)
807 finally: 867 finally:
850 def _useobsolete(self): 910 def _useobsolete(self):
851 """() -> bool""" 911 """() -> bool"""
852 return obsolete.isenabled(self.repo, obsolete.createmarkersopt) 912 return obsolete.isenabled(self.repo, obsolete.createmarkersopt)
853 913
854 def _cleanupoldcommits(self): 914 def _cleanupoldcommits(self):
855 replacements = {k: ([v] if v is not None else []) 915 replacements = {
856 for k, v in self.replacemap.iteritems()} 916 k: ([v] if v is not None else [])
917 for k, v in self.replacemap.iteritems()
918 }
857 if replacements: 919 if replacements:
858 scmutil.cleanupnodes(self.repo, replacements, operation='absorb', 920 scmutil.cleanupnodes(
859 fixphase=True) 921 self.repo, replacements, operation='absorb', fixphase=True
922 )
923
860 924
861 def _parsechunk(hunk): 925 def _parsechunk(hunk):
862 """(crecord.uihunk or patch.recordhunk) -> (path, (a1, a2, [bline]))""" 926 """(crecord.uihunk or patch.recordhunk) -> (path, (a1, a2, [bline]))"""
863 if type(hunk) not in (crecord.uihunk, patch.recordhunk): 927 if type(hunk) not in (crecord.uihunk, patch.recordhunk):
864 return None, None 928 return None, None
872 # hunk.prettystr() will update hunk.removed 936 # hunk.prettystr() will update hunk.removed
873 a2 = a1 + hunk.removed 937 a2 = a1 + hunk.removed
874 blines = [l[1:] for l in patchlines[1:] if not l.startswith('-')] 938 blines = [l[1:] for l in patchlines[1:] if not l.startswith('-')]
875 return path, (a1, a2, blines) 939 return path, (a1, a2, blines)
876 940
941
877 def overlaydiffcontext(ctx, chunks): 942 def overlaydiffcontext(ctx, chunks):
878 """(ctx, [crecord.uihunk]) -> memctx 943 """(ctx, [crecord.uihunk]) -> memctx
879 944
880 return a memctx with some [1] patches (chunks) applied to ctx. 945 return a memctx with some [1] patches (chunks) applied to ctx.
881 [1]: modifications are handled. renames, mode changes, etc. are ignored. 946 [1]: modifications are handled. renames, mode changes, etc. are ignored.
887 # 2. a lot of different implementations about "chunk" (patch.hunk, 952 # 2. a lot of different implementations about "chunk" (patch.hunk,
888 # patch.recordhunk, crecord.uihunk) 953 # patch.recordhunk, crecord.uihunk)
889 # as we only care about applying changes to modified files, no mode 954 # as we only care about applying changes to modified files, no mode
890 # change, no binary diff, and no renames, it's probably okay to 955 # change, no binary diff, and no renames, it's probably okay to
891 # re-invent the logic using much simpler code here. 956 # re-invent the logic using much simpler code here.
892 memworkingcopy = {} # {path: content} 957 memworkingcopy = {} # {path: content}
893 patchmap = defaultdict(lambda: []) # {path: [(a1, a2, [bline])]} 958 patchmap = defaultdict(lambda: []) # {path: [(a1, a2, [bline])]}
894 for path, info in map(_parsechunk, chunks): 959 for path, info in map(_parsechunk, chunks):
895 if not path or not info: 960 if not path or not info:
896 continue 961 continue
897 patchmap[path].append(info) 962 patchmap[path].append(info)
898 for path, patches in patchmap.iteritems(): 963 for path, patches in patchmap.iteritems():
903 for a1, a2, blines in patches: 968 for a1, a2, blines in patches:
904 lines[a1:a2] = blines 969 lines[a1:a2] = blines
905 memworkingcopy[path] = ''.join(lines) 970 memworkingcopy[path] = ''.join(lines)
906 return overlaycontext(memworkingcopy, ctx) 971 return overlaycontext(memworkingcopy, ctx)
907 972
973
908 def absorb(ui, repo, stack=None, targetctx=None, pats=None, opts=None): 974 def absorb(ui, repo, stack=None, targetctx=None, pats=None, opts=None):
909 """pick fixup chunks from targetctx, apply them to stack. 975 """pick fixup chunks from targetctx, apply them to stack.
910 976
911 if targetctx is None, the working copy context will be used. 977 if targetctx is None, the working copy context will be used.
912 if stack is None, the current draft stack will be used. 978 if stack is None, the current draft stack will be used.
917 headctx = repo['.'] 983 headctx = repo['.']
918 if len(headctx.parents()) > 1: 984 if len(headctx.parents()) > 1:
919 raise error.Abort(_('cannot absorb into a merge')) 985 raise error.Abort(_('cannot absorb into a merge'))
920 stack = getdraftstack(headctx, limit) 986 stack = getdraftstack(headctx, limit)
921 if limit and len(stack) >= limit: 987 if limit and len(stack) >= limit:
922 ui.warn(_('absorb: only the recent %d changesets will ' 988 ui.warn(
923 'be analysed\n') 989 _('absorb: only the recent %d changesets will ' 'be analysed\n')
924 % limit) 990 % limit
991 )
925 if not stack: 992 if not stack:
926 raise error.Abort(_('no mutable changeset to change')) 993 raise error.Abort(_('no mutable changeset to change'))
927 if targetctx is None: # default to working copy 994 if targetctx is None: # default to working copy
928 targetctx = repo[None] 995 targetctx = repo[None]
929 if pats is None: 996 if pats is None:
930 pats = () 997 pats = ()
931 if opts is None: 998 if opts is None:
932 opts = {} 999 opts = {}
951 fm.startitem() 1018 fm.startitem()
952 fm.context(ctx=ctx) 1019 fm.context(ctx=ctx)
953 fm.data(linetype='changeset') 1020 fm.data(linetype='changeset')
954 fm.write('node', '%-7.7s ', ctx.hex(), label='absorb.node') 1021 fm.write('node', '%-7.7s ', ctx.hex(), label='absorb.node')
955 descfirstline = ctx.description().splitlines()[0] 1022 descfirstline = ctx.description().splitlines()[0]
956 fm.write('descfirstline', '%s\n', descfirstline, 1023 fm.write(
957 label='absorb.description') 1024 'descfirstline',
1025 '%s\n',
1026 descfirstline,
1027 label='absorb.description',
1028 )
958 fm.end() 1029 fm.end()
959 if not opts.get('dry_run'): 1030 if not opts.get('dry_run'):
960 if (not opts.get('apply_changes') and 1031 if (
961 state.ctxaffected and 1032 not opts.get('apply_changes')
962 ui.promptchoice("apply changes (yn)? $$ &Yes $$ &No", default=1)): 1033 and state.ctxaffected
1034 and ui.promptchoice("apply changes (yn)? $$ &Yes $$ &No", default=1)
1035 ):
963 raise error.Abort(_('absorb cancelled\n')) 1036 raise error.Abort(_('absorb cancelled\n'))
964 1037
965 state.apply() 1038 state.apply()
966 if state.commit(): 1039 if state.commit():
967 state.printchunkstats() 1040 state.printchunkstats()
968 elif not ui.quiet: 1041 elif not ui.quiet:
969 ui.write(_('nothing applied\n')) 1042 ui.write(_('nothing applied\n'))
970 return state 1043 return state
971 1044
972 @command('absorb', 1045
973 [('a', 'apply-changes', None, 1046 @command(
974 _('apply changes without prompting for confirmation')), 1047 'absorb',
975 ('p', 'print-changes', None, 1048 [
976 _('always print which changesets are modified by which changes')), 1049 (
977 ('i', 'interactive', None, 1050 'a',
978 _('interactively select which chunks to apply (EXPERIMENTAL)')), 1051 'apply-changes',
979 ('e', 'edit-lines', None, 1052 None,
980 _('edit what lines belong to which changesets before commit ' 1053 _('apply changes without prompting for confirmation'),
981 '(EXPERIMENTAL)')), 1054 ),
982 ] + commands.dryrunopts + commands.templateopts + commands.walkopts, 1055 (
983 _('hg absorb [OPTION] [FILE]...'), 1056 'p',
984 helpcategory=command.CATEGORY_COMMITTING, 1057 'print-changes',
985 helpbasic=True) 1058 None,
1059 _('always print which changesets are modified by which changes'),
1060 ),
1061 (
1062 'i',
1063 'interactive',
1064 None,
1065 _('interactively select which chunks to apply (EXPERIMENTAL)'),
1066 ),
1067 (
1068 'e',
1069 'edit-lines',
1070 None,
1071 _(
1072 'edit what lines belong to which changesets before commit '
1073 '(EXPERIMENTAL)'
1074 ),
1075 ),
1076 ]
1077 + commands.dryrunopts
1078 + commands.templateopts
1079 + commands.walkopts,
1080 _('hg absorb [OPTION] [FILE]...'),
1081 helpcategory=command.CATEGORY_COMMITTING,
1082 helpbasic=True,
1083 )
986 def absorbcmd(ui, repo, *pats, **opts): 1084 def absorbcmd(ui, repo, *pats, **opts):
987 """incorporate corrections into the stack of draft changesets 1085 """incorporate corrections into the stack of draft changesets
988 1086
989 absorb analyzes each change in your working directory and attempts to 1087 absorb analyzes each change in your working directory and attempts to
990 amend the changed lines into the changesets in your stack that first 1088 amend the changed lines into the changesets in your stack that first