Mercurial > hg
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 |