##// END OF EJS Templates
py3: fix kwargs handling in hgext/absorb.py...
Pulkit Goyal -
r39822:d50125de default
parent child Browse files
Show More
@@ -1,571 +1,573 b''
1 1 test-abort-checkin.t
2 2 test-absorb-filefixupstate.py
3 3 test-absorb-phase.t
4 test-absorb-rename.t
4 5 test-absorb-strip.t
6 test-absorb.t
5 7 test-add.t
6 8 test-addremove-similar.t
7 9 test-addremove.t
8 10 test-alias.t
9 11 test-amend-subrepo.t
10 12 test-amend.t
11 13 test-ancestor.py
12 14 test-annotate.py
13 15 test-annotate.t
14 16 test-archive-symlinks.t
15 17 test-atomictempfile.py
16 18 test-audit-path.t
17 19 test-audit-subrepo.t
18 20 test-automv.t
19 21 test-backout.t
20 22 test-backwards-remove.t
21 23 test-bad-pull.t
22 24 test-basic.t
23 25 test-bheads.t
24 26 test-bisect.t
25 27 test-bisect2.t
26 28 test-bisect3.t
27 29 test-blackbox.t
28 30 test-bookmarks-current.t
29 31 test-bookmarks-merge.t
30 32 test-bookmarks-pushpull.t
31 33 test-bookmarks-rebase.t
32 34 test-bookmarks-strip.t
33 35 test-bookmarks.t
34 36 test-branch-change.t
35 37 test-branch-option.t
36 38 test-branch-tag-confict.t
37 39 test-branches.t
38 40 test-bundle-phases.t
39 41 test-bundle-r.t
40 42 test-bundle-type.t
41 43 test-bundle-vs-outgoing.t
42 44 test-bundle.t
43 45 test-bundle2-exchange.t
44 46 test-bundle2-format.t
45 47 test-bundle2-multiple-changegroups.t
46 48 test-bundle2-pushback.t
47 49 test-bundle2-remote-changegroup.t
48 50 test-cappedreader.py
49 51 test-casecollision.t
50 52 test-cat.t
51 53 test-cbor.py
52 54 test-censor.t
53 55 test-changelog-exec.t
54 56 test-check-code.t
55 57 test-check-commit.t
56 58 test-check-execute.t
57 59 test-check-interfaces.py
58 60 test-check-module-imports.t
59 61 test-check-py3-compat.t
60 62 test-check-pyflakes.t
61 63 test-check-pylint.t
62 64 test-check-shbang.t
63 65 test-children.t
64 66 test-clone-cgi.t
65 67 test-clone-pull-corruption.t
66 68 test-clone-r.t
67 69 test-clone-update-order.t
68 70 test-clonebundles.t
69 71 test-commit-amend.t
70 72 test-commit-interactive.t
71 73 test-commit-multiple.t
72 74 test-commit-unresolved.t
73 75 test-commit.t
74 76 test-committer.t
75 77 test-completion.t
76 78 test-config-env.py
77 79 test-config.t
78 80 test-conflict.t
79 81 test-confused-revert.t
80 82 test-context.py
81 83 test-contrib-check-code.t
82 84 test-contrib-check-commit.t
83 85 test-convert-authormap.t
84 86 test-convert-clonebranches.t
85 87 test-convert-cvs-branch.t
86 88 test-convert-cvs-detectmerge.t
87 89 test-convert-cvs-synthetic.t
88 90 test-convert-cvs.t
89 91 test-convert-cvsnt-mergepoints.t
90 92 test-convert-datesort.t
91 93 test-convert-filemap.t
92 94 test-convert-hg-sink.t
93 95 test-convert-hg-source.t
94 96 test-convert-hg-startrev.t
95 97 test-convert-splicemap.t
96 98 test-convert-tagsbranch-topology.t
97 99 test-copy-move-merge.t
98 100 test-copy.t
99 101 test-copytrace-heuristics.t
100 102 test-debugbuilddag.t
101 103 test-debugbundle.t
102 104 test-debugextensions.t
103 105 test-debugindexdot.t
104 106 test-debugrename.t
105 107 test-default-push.t
106 108 test-diff-antipatience.t
107 109 test-diff-binary-file.t
108 110 test-diff-change.t
109 111 test-diff-copy-depth.t
110 112 test-diff-hashes.t
111 113 test-diff-ignore-whitespace.t
112 114 test-diff-indent-heuristic.t
113 115 test-diff-issue2761.t
114 116 test-diff-newlines.t
115 117 test-diff-reverse.t
116 118 test-diff-subdir.t
117 119 test-diff-unified.t
118 120 test-diff-upgrade.t
119 121 test-diffdir.t
120 122 test-diffstat.t
121 123 test-directaccess.t
122 124 test-dirstate-backup.t
123 125 test-dirstate-nonnormalset.t
124 126 test-dirstate.t
125 127 test-dispatch.py
126 128 test-doctest.py
127 129 test-double-merge.t
128 130 test-drawdag.t
129 131 test-duplicateoptions.py
130 132 test-editor-filename.t
131 133 test-empty-dir.t
132 134 test-empty-file.t
133 135 test-empty-group.t
134 136 test-empty.t
135 137 test-encode.t
136 138 test-encoding-func.py
137 139 test-encoding.t
138 140 test-eol-add.t
139 141 test-eol-clone.t
140 142 test-eol-hook.t
141 143 test-eol-patch.t
142 144 test-eol-tag.t
143 145 test-eol-update.t
144 146 test-eol.t
145 147 test-eolfilename.t
146 148 test-excessive-merge.t
147 149 test-exchange-obsmarkers-case-A1.t
148 150 test-exchange-obsmarkers-case-A2.t
149 151 test-exchange-obsmarkers-case-A3.t
150 152 test-exchange-obsmarkers-case-A4.t
151 153 test-exchange-obsmarkers-case-A5.t
152 154 test-exchange-obsmarkers-case-A6.t
153 155 test-exchange-obsmarkers-case-A7.t
154 156 test-exchange-obsmarkers-case-B1.t
155 157 test-exchange-obsmarkers-case-B2.t
156 158 test-exchange-obsmarkers-case-B3.t
157 159 test-exchange-obsmarkers-case-B4.t
158 160 test-exchange-obsmarkers-case-B5.t
159 161 test-exchange-obsmarkers-case-B6.t
160 162 test-exchange-obsmarkers-case-B7.t
161 163 test-exchange-obsmarkers-case-C1.t
162 164 test-exchange-obsmarkers-case-C2.t
163 165 test-exchange-obsmarkers-case-C3.t
164 166 test-exchange-obsmarkers-case-C4.t
165 167 test-exchange-obsmarkers-case-D1.t
166 168 test-exchange-obsmarkers-case-D2.t
167 169 test-exchange-obsmarkers-case-D3.t
168 170 test-exchange-obsmarkers-case-D4.t
169 171 test-execute-bit.t
170 172 test-export.t
171 173 test-extdata.t
172 174 test-extdiff.t
173 175 test-extensions-afterloaded.t
174 176 test-extensions-wrapfunction.py
175 177 test-extra-filelog-entry.t
176 178 test-fetch.t
177 179 test-filebranch.t
178 180 test-filecache.py
179 181 test-filelog.py
180 182 test-fileset-generated.t
181 183 test-fileset.t
182 184 test-fix-topology.t
183 185 test-flags.t
184 186 test-generaldelta.t
185 187 test-getbundle.t
186 188 test-git-export.t
187 189 test-glog-beautifygraph.t
188 190 test-glog-topological.t
189 191 test-glog.t
190 192 test-gpg.t
191 193 test-graft.t
192 194 test-grep.t
193 195 test-hg-parseurl.py
194 196 test-hghave.t
195 197 test-hgignore.t
196 198 test-hgk.t
197 199 test-hgrc.t
198 200 test-hgweb-bundle.t
199 201 test-hgweb-descend-empties.t
200 202 test-hgweb-empty.t
201 203 test-hgweb-removed.t
202 204 test-hgwebdir-paths.py
203 205 test-hgwebdirsym.t
204 206 test-histedit-arguments.t
205 207 test-histedit-base.t
206 208 test-histedit-bookmark-motion.t
207 209 test-histedit-commute.t
208 210 test-histedit-drop.t
209 211 test-histedit-edit.t
210 212 test-histedit-fold-non-commute.t
211 213 test-histedit-fold.t
212 214 test-histedit-no-backup.t
213 215 test-histedit-no-change.t
214 216 test-histedit-non-commute-abort.t
215 217 test-histedit-non-commute.t
216 218 test-histedit-obsolete.t
217 219 test-histedit-outgoing.t
218 220 test-histedit-templates.t
219 221 test-http-branchmap.t
220 222 test-http-bundle1.t
221 223 test-http-clone-r.t
222 224 test-http-permissions.t
223 225 test-http.t
224 226 test-hybridencode.py
225 227 test-identify.t
226 228 test-impexp-branch.t
227 229 test-import-bypass.t
228 230 test-import-eol.t
229 231 test-import-merge.t
230 232 test-import-unknown.t
231 233 test-import.t
232 234 test-imports-checker.t
233 235 test-incoming-outgoing.t
234 236 test-inherit-mode.t
235 237 test-init.t
236 238 test-issue1089.t
237 239 test-issue1102.t
238 240 test-issue1175.t
239 241 test-issue1306.t
240 242 test-issue1438.t
241 243 test-issue1502.t
242 244 test-issue1802.t
243 245 test-issue1877.t
244 246 test-issue1993.t
245 247 test-issue2137.t
246 248 test-issue3084.t
247 249 test-issue4074.t
248 250 test-issue522.t
249 251 test-issue586.t
250 252 test-issue5979.t
251 253 test-issue612.t
252 254 test-issue619.t
253 255 test-issue660.t
254 256 test-issue672.t
255 257 test-issue842.t
256 258 test-journal-exists.t
257 259 test-journal-share.t
258 260 test-journal.t
259 261 test-known.t
260 262 test-largefiles-cache.t
261 263 test-largefiles-misc.t
262 264 test-largefiles-small-disk.t
263 265 test-largefiles-update.t
264 266 test-largefiles.t
265 267 test-lfs-largefiles.t
266 268 test-lfs-pointer.py
267 269 test-linelog.py
268 270 test-linerange.py
269 271 test-locate.t
270 272 test-lock-badness.t
271 273 test-log-linerange.t
272 274 test-log.t
273 275 test-logexchange.t
274 276 test-lrucachedict.py
275 277 test-mactext.t
276 278 test-mailmap.t
277 279 test-manifest-merging.t
278 280 test-manifest.py
279 281 test-manifest.t
280 282 test-match.py
281 283 test-mdiff.py
282 284 test-merge-changedelete.t
283 285 test-merge-closedheads.t
284 286 test-merge-commit.t
285 287 test-merge-criss-cross.t
286 288 test-merge-default.t
287 289 test-merge-force.t
288 290 test-merge-halt.t
289 291 test-merge-internal-tools-pattern.t
290 292 test-merge-local.t
291 293 test-merge-no-file-change.t
292 294 test-merge-remove.t
293 295 test-merge-revert.t
294 296 test-merge-revert2.t
295 297 test-merge-subrepos.t
296 298 test-merge-symlinks.t
297 299 test-merge-tools.t
298 300 test-merge-types.t
299 301 test-merge1.t
300 302 test-merge10.t
301 303 test-merge2.t
302 304 test-merge4.t
303 305 test-merge5.t
304 306 test-merge6.t
305 307 test-merge7.t
306 308 test-merge8.t
307 309 test-merge9.t
308 310 test-minifileset.py
309 311 test-minirst.py
310 312 test-mq-git.t
311 313 test-mq-guards.t
312 314 test-mq-header-date.t
313 315 test-mq-header-from.t
314 316 test-mq-merge.t
315 317 test-mq-pull-from-bundle.t
316 318 test-mq-qclone-http.t
317 319 test-mq-qdelete.t
318 320 test-mq-qdiff.t
319 321 test-mq-qfold.t
320 322 test-mq-qgoto.t
321 323 test-mq-qimport-fail-cleanup.t
322 324 test-mq-qnew.t
323 325 test-mq-qpush-exact.t
324 326 test-mq-qpush-fail.t
325 327 test-mq-qqueue.t
326 328 test-mq-qrefresh-interactive.t
327 329 test-mq-qrefresh-replace-log-message.t
328 330 test-mq-qrefresh.t
329 331 test-mq-qrename.t
330 332 test-mq-qsave.t
331 333 test-mq-safety.t
332 334 test-mq-subrepo.t
333 335 test-mq-symlinks.t
334 336 test-mq.t
335 337 test-mv-cp-st-diff.t
336 338 test-narrow-acl.t
337 339 test-narrow-archive.t
338 340 test-narrow-clone-no-ellipsis.t
339 341 test-narrow-clone-non-narrow-server.t
340 342 test-narrow-clone-nonlinear.t
341 343 test-narrow-clone.t
342 344 test-narrow-commit.t
343 345 test-narrow-copies.t
344 346 test-narrow-debugcommands.t
345 347 test-narrow-debugrebuilddirstate.t
346 348 test-narrow-exchange-merges.t
347 349 test-narrow-exchange.t
348 350 test-narrow-expanddirstate.t
349 351 test-narrow-merge.t
350 352 test-narrow-patch.t
351 353 test-narrow-patterns.t
352 354 test-narrow-pull.t
353 355 test-narrow-rebase.t
354 356 test-narrow-shallow-merges.t
355 357 test-narrow-shallow.t
356 358 test-narrow-strip.t
357 359 test-narrow-trackedcmd.t
358 360 test-narrow-update.t
359 361 test-narrow-widen-no-ellipsis.t
360 362 test-narrow-widen.t
361 363 test-narrow.t
362 364 test-nested-repo.t
363 365 test-newbranch.t
364 366 test-newercgi.t
365 367 test-nointerrupt.t
366 368 test-obshistory.t
367 369 test-obsmarker-template.t
368 370 test-obsmarkers-effectflag.t
369 371 test-obsolete-bounds-checking.t
370 372 test-obsolete-bundle-strip.t
371 373 test-obsolete-changeset-exchange.t
372 374 test-obsolete-checkheads.t
373 375 test-obsolete-distributed.t
374 376 test-obsolete-divergent.t
375 377 test-obsolete-tag-cache.t
376 378 test-pager.t
377 379 test-parents.t
378 380 test-parseindex2.py
379 381 test-patch-offset.t
380 382 test-patch.t
381 383 test-patchbomb-bookmark.t
382 384 test-patchbomb-tls.t
383 385 test-patchbomb.t
384 386 test-pathconflicts-merge.t
385 387 test-pathconflicts-update.t
386 388 test-pathencode.py
387 389 test-pending.t
388 390 test-permissions.t
389 391 test-phases.t
390 392 test-pull-branch.t
391 393 test-pull-http.t
392 394 test-pull-permission.t
393 395 test-pull-pull-corruption.t
394 396 test-pull-r.t
395 397 test-pull-update.t
396 398 test-pull.t
397 399 test-purge.t
398 400 test-push-cgi.t
399 401 test-push-checkheads-partial-C1.t
400 402 test-push-checkheads-partial-C2.t
401 403 test-push-checkheads-partial-C3.t
402 404 test-push-checkheads-partial-C4.t
403 405 test-push-checkheads-pruned-B1.t
404 406 test-push-checkheads-pruned-B2.t
405 407 test-push-checkheads-pruned-B3.t
406 408 test-push-checkheads-pruned-B4.t
407 409 test-push-checkheads-pruned-B5.t
408 410 test-push-checkheads-pruned-B6.t
409 411 test-push-checkheads-pruned-B7.t
410 412 test-push-checkheads-pruned-B8.t
411 413 test-push-checkheads-superceed-A1.t
412 414 test-push-checkheads-superceed-A2.t
413 415 test-push-checkheads-superceed-A3.t
414 416 test-push-checkheads-superceed-A4.t
415 417 test-push-checkheads-superceed-A5.t
416 418 test-push-checkheads-superceed-A6.t
417 419 test-push-checkheads-superceed-A7.t
418 420 test-push-checkheads-superceed-A8.t
419 421 test-push-checkheads-unpushed-D1.t
420 422 test-push-checkheads-unpushed-D2.t
421 423 test-push-checkheads-unpushed-D3.t
422 424 test-push-checkheads-unpushed-D4.t
423 425 test-push-checkheads-unpushed-D5.t
424 426 test-push-checkheads-unpushed-D6.t
425 427 test-push-checkheads-unpushed-D7.t
426 428 test-push-http.t
427 429 test-push-warn.t
428 430 test-push.t
429 431 test-pushvars.t
430 432 test-qrecord.t
431 433 test-rebase-abort.t
432 434 test-rebase-backup.t
433 435 test-rebase-base-flag.t
434 436 test-rebase-bookmarks.t
435 437 test-rebase-brute-force.t
436 438 test-rebase-cache.t
437 439 test-rebase-check-restore.t
438 440 test-rebase-collapse.t
439 441 test-rebase-conflicts.t
440 442 test-rebase-dest.t
441 443 test-rebase-detach.t
442 444 test-rebase-emptycommit.t
443 445 test-rebase-inmemory.t
444 446 test-rebase-interruptions.t
445 447 test-rebase-issue-noparam-single-rev.t
446 448 test-rebase-legacy.t
447 449 test-rebase-mq-skip.t
448 450 test-rebase-mq.t
449 451 test-rebase-named-branches.t
450 452 test-rebase-newancestor.t
451 453 test-rebase-obsolete.t
452 454 test-rebase-parameters.t
453 455 test-rebase-partial.t
454 456 test-rebase-pull.t
455 457 test-rebase-rename.t
456 458 test-rebase-scenario-global.t
457 459 test-rebase-templates.t
458 460 test-rebase-transaction.t
459 461 test-rebuildstate.t
460 462 test-record.t
461 463 test-relink.t
462 464 test-remove.t
463 465 test-removeemptydirs.t
464 466 test-rename-after-merge.t
465 467 test-rename-dir-merge.t
466 468 test-rename-merge1.t
467 469 test-rename-merge2.t
468 470 test-rename.t
469 471 test-repair-strip.t
470 472 test-repo-compengines.t
471 473 test-resolve.t
472 474 test-revert-flags.t
473 475 test-revert-interactive.t
474 476 test-revert-unknown.t
475 477 test-revert.t
476 478 test-revisions.t
477 479 test-revlog-ancestry.py
478 480 test-revlog-group-emptyiter.t
479 481 test-revlog-mmapindex.t
480 482 test-revlog-packentry.t
481 483 test-revlog-raw.py
482 484 test-revlog-v2.t
483 485 test-revset-dirstate-parents.t
484 486 test-revset-legacy-lookup.t
485 487 test-revset-outgoing.t
486 488 test-rollback.t
487 489 test-run-tests.py
488 490 test-run-tests.t
489 491 test-schemes.t
490 492 test-serve.t
491 493 test-setdiscovery.t
492 494 test-share.t
493 495 test-shelve.t
494 496 test-show-stack.t
495 497 test-show-work.t
496 498 test-show.t
497 499 test-simple-update.t
498 500 test-simplekeyvaluefile.py
499 501 test-simplemerge.py
500 502 test-single-head.t
501 503 test-sparse-clear.t
502 504 test-sparse-clone.t
503 505 test-sparse-import.t
504 506 test-sparse-merges.t
505 507 test-sparse-profiles.t
506 508 test-sparse-requirement.t
507 509 test-sparse-verbose-json.t
508 510 test-sparse.t
509 511 test-split.t
510 512 test-ssh-bundle1.t
511 513 test-ssh-clone-r.t
512 514 test-ssh-proto-unbundle.t
513 515 test-ssh-proto.t
514 516 test-ssh.t
515 517 test-sshserver.py
516 518 test-stack.t
517 519 test-status-inprocess.py
518 520 test-status-rev.t
519 521 test-status-terse.t
520 522 test-strict.t
521 523 test-strip-cross.t
522 524 test-strip.t
523 525 test-subrepo-deep-nested-change.t
524 526 test-subrepo-missing.t
525 527 test-subrepo-paths.t
526 528 test-subrepo-recursion.t
527 529 test-subrepo-relative-path.t
528 530 test-subrepo.t
529 531 test-symlink-os-yes-fs-no.py
530 532 test-symlink-placeholder.t
531 533 test-symlinks.t
532 534 test-tag.t
533 535 test-tags.t
534 536 test-template-basic.t
535 537 test-template-functions.t
536 538 test-template-keywords.t
537 539 test-template-map.t
538 540 test-transplant.t
539 541 test-treemanifest.t
540 542 test-ui-color.py
541 543 test-ui-config.py
542 544 test-ui-verbosity.py
543 545 test-unamend.t
544 546 test-unbundlehash.t
545 547 test-uncommit.t
546 548 test-unified-test.t
547 549 test-unionrepo.t
548 550 test-unrelated-pull.t
549 551 test-up-local-change.t
550 552 test-update-branches.t
551 553 test-update-dest.t
552 554 test-update-issue1456.t
553 555 test-update-names.t
554 556 test-update-reverse.t
555 557 test-upgrade-repo.t
556 558 test-url-download.t
557 559 test-url-rev.t
558 560 test-url.py
559 561 test-username-newline.t
560 562 test-util.py
561 563 test-verify.t
562 564 test-walk.t
563 565 test-walkrepo.py
564 566 test-websub.t
565 567 test-win32text.t
566 568 test-wireproto-clientreactor.py
567 569 test-wireproto-framing.py
568 570 test-wireproto-serverreactor.py
569 571 test-wireproto.py
570 572 test-wsgirequest.py
571 573 test-xdg.t
@@ -1,977 +1,978 b''
1 1 # absorb.py
2 2 #
3 3 # Copyright 2016 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """apply working directory changes to changesets (EXPERIMENTAL)
9 9
10 10 The absorb extension provides a command to use annotate information to
11 11 amend modified chunks into the corresponding non-public changesets.
12 12
13 13 ::
14 14
15 15 [absorb]
16 16 # only check 50 recent non-public changesets at most
17 17 max-stack-size = 50
18 18 # whether to add noise to new commits to avoid obsolescence cycle
19 19 add-noise = 1
20 20 # make `amend --correlated` a shortcut to the main command
21 21 amend-flag = correlated
22 22
23 23 [color]
24 24 absorb.node = blue bold
25 25 absorb.path = bold
26 26 """
27 27
28 28 # TODO:
29 29 # * Rename config items to [commands] namespace
30 30 # * Converge getdraftstack() with other code in core
31 31 # * move many attributes on fixupstate to be private
32 32
33 33 from __future__ import absolute_import
34 34
35 35 import collections
36 36
37 37 from mercurial.i18n import _
38 38 from mercurial import (
39 39 cmdutil,
40 40 commands,
41 41 context,
42 42 crecord,
43 43 error,
44 44 linelog,
45 45 mdiff,
46 46 node,
47 47 obsolete,
48 48 patch,
49 49 phases,
50 50 pycompat,
51 51 registrar,
52 52 repair,
53 53 scmutil,
54 54 util,
55 55 )
56 56 from mercurial.utils import (
57 57 stringutil,
58 58 )
59 59
60 60 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
61 61 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
62 62 # be specifying the version(s) of Mercurial they are tested with, or
63 63 # leave the attribute unspecified.
64 64 testedwith = 'ships-with-hg-core'
65 65
66 66 cmdtable = {}
67 67 command = registrar.command(cmdtable)
68 68
69 69 configtable = {}
70 70 configitem = registrar.configitem(configtable)
71 71
72 72 configitem('absorb', 'add-noise', default=True)
73 73 configitem('absorb', 'amend-flag', default=None)
74 74 configitem('absorb', 'max-stack-size', default=50)
75 75
76 76 colortable = {
77 77 'absorb.node': 'blue bold',
78 78 'absorb.path': 'bold',
79 79 }
80 80
81 81 defaultdict = collections.defaultdict
82 82
83 83 class nullui(object):
84 84 """blank ui object doing nothing"""
85 85 debugflag = False
86 86 verbose = False
87 87 quiet = True
88 88
89 89 def __getitem__(name):
90 90 def nullfunc(*args, **kwds):
91 91 return
92 92 return nullfunc
93 93
94 94 class emptyfilecontext(object):
95 95 """minimal filecontext representing an empty file"""
96 96 def data(self):
97 97 return ''
98 98
99 99 def node(self):
100 100 return node.nullid
101 101
102 102 def uniq(lst):
103 103 """list -> list. remove duplicated items without changing the order"""
104 104 seen = set()
105 105 result = []
106 106 for x in lst:
107 107 if x not in seen:
108 108 seen.add(x)
109 109 result.append(x)
110 110 return result
111 111
112 112 def getdraftstack(headctx, limit=None):
113 113 """(ctx, int?) -> [ctx]. get a linear stack of non-public changesets.
114 114
115 115 changesets are sorted in topo order, oldest first.
116 116 return at most limit items, if limit is a positive number.
117 117
118 118 merges are considered as non-draft as well. i.e. every commit
119 119 returned has and only has 1 parent.
120 120 """
121 121 ctx = headctx
122 122 result = []
123 123 while ctx.phase() != phases.public:
124 124 if limit and len(result) >= limit:
125 125 break
126 126 parents = ctx.parents()
127 127 if len(parents) != 1:
128 128 break
129 129 result.append(ctx)
130 130 ctx = parents[0]
131 131 result.reverse()
132 132 return result
133 133
134 134 def getfilestack(stack, path, seenfctxs=None):
135 135 """([ctx], str, set) -> [fctx], {ctx: fctx}
136 136
137 137 stack is a list of contexts, from old to new. usually they are what
138 138 "getdraftstack" returns.
139 139
140 140 follows renames, but not copies.
141 141
142 142 seenfctxs is a set of filecontexts that will be considered "immutable".
143 143 they are usually what this function returned in earlier calls, useful
144 144 to avoid issues that a file was "moved" to multiple places and was then
145 145 modified differently, like: "a" was copied to "b", "a" was also copied to
146 146 "c" and then "a" was deleted, then both "b" and "c" were "moved" from "a"
147 147 and we enforce only one of them to be able to affect "a"'s content.
148 148
149 149 return an empty list and an empty dict, if the specified path does not
150 150 exist in stack[-1] (the top of the stack).
151 151
152 152 otherwise, return a list of de-duplicated filecontexts, and the map to
153 153 convert ctx in the stack to fctx, for possible mutable fctxs. the first item
154 154 of the list would be outside the stack and should be considered immutable.
155 155 the remaining items are within the stack.
156 156
157 157 for example, given the following changelog and corresponding filelog
158 158 revisions:
159 159
160 160 changelog: 3----4----5----6----7
161 161 filelog: x 0----1----1----2 (x: no such file yet)
162 162
163 163 - if stack = [5, 6, 7], returns ([0, 1, 2], {5: 1, 6: 1, 7: 2})
164 164 - if stack = [3, 4, 5], returns ([e, 0, 1], {4: 0, 5: 1}), where "e" is a
165 165 dummy empty filecontext.
166 166 - if stack = [2], returns ([], {})
167 167 - if stack = [7], returns ([1, 2], {7: 2})
168 168 - if stack = [6, 7], returns ([1, 2], {6: 1, 7: 2}), although {6: 1} can be
169 169 removed, since 1 is immutable.
170 170 """
171 171 if seenfctxs is None:
172 172 seenfctxs = set()
173 173 assert stack
174 174
175 175 if path not in stack[-1]:
176 176 return [], {}
177 177
178 178 fctxs = []
179 179 fctxmap = {}
180 180
181 181 pctx = stack[0].p1() # the public (immutable) ctx we stop at
182 182 for ctx in reversed(stack):
183 183 if path not in ctx: # the file is added in the next commit
184 184 pctx = ctx
185 185 break
186 186 fctx = ctx[path]
187 187 fctxs.append(fctx)
188 188 if fctx in seenfctxs: # treat fctx as the immutable one
189 189 pctx = None # do not add another immutable fctx
190 190 break
191 191 fctxmap[ctx] = fctx # only for mutable fctxs
192 192 renamed = fctx.renamed()
193 193 if renamed:
194 194 path = renamed[0] # follow rename
195 195 if path in ctx: # but do not follow copy
196 196 pctx = ctx.p1()
197 197 break
198 198
199 199 if pctx is not None: # need an extra immutable fctx
200 200 if path in pctx:
201 201 fctxs.append(pctx[path])
202 202 else:
203 203 fctxs.append(emptyfilecontext())
204 204
205 205 fctxs.reverse()
206 206 # note: we rely on a property of hg: filerev is not reused for linear
207 207 # history. i.e. it's impossible to have:
208 208 # changelog: 4----5----6 (linear, no merges)
209 209 # filelog: 1----2----1
210 210 # ^ reuse filerev (impossible)
211 211 # because parents are part of the hash. if that's not true, we need to
212 212 # remove uniq and find a different way to identify fctxs.
213 213 return uniq(fctxs), fctxmap
214 214
215 215 class overlaystore(patch.filestore):
216 216 """read-only, hybrid store based on a dict and ctx.
217 217 memworkingcopy: {path: content}, overrides file contents.
218 218 """
219 219 def __init__(self, basectx, memworkingcopy):
220 220 self.basectx = basectx
221 221 self.memworkingcopy = memworkingcopy
222 222
223 223 def getfile(self, path):
224 224 """comply with mercurial.patch.filestore.getfile"""
225 225 if path not in self.basectx:
226 226 return None, None, None
227 227 fctx = self.basectx[path]
228 228 if path in self.memworkingcopy:
229 229 content = self.memworkingcopy[path]
230 230 else:
231 231 content = fctx.data()
232 232 mode = (fctx.islink(), fctx.isexec())
233 233 renamed = fctx.renamed() # False or (path, node)
234 234 return content, mode, (renamed and renamed[0])
235 235
236 236 def overlaycontext(memworkingcopy, ctx, parents=None, extra=None):
237 237 """({path: content}, ctx, (p1node, p2node)?, {}?) -> memctx
238 238 memworkingcopy overrides file contents.
239 239 """
240 240 # parents must contain 2 items: (node1, node2)
241 241 if parents is None:
242 242 parents = ctx.repo().changelog.parents(ctx.node())
243 243 if extra is None:
244 244 extra = ctx.extra()
245 245 date = ctx.date()
246 246 desc = ctx.description()
247 247 user = ctx.user()
248 248 files = set(ctx.files()).union(memworkingcopy)
249 249 store = overlaystore(ctx, memworkingcopy)
250 250 return context.memctx(
251 251 repo=ctx.repo(), parents=parents, text=desc,
252 252 files=files, filectxfn=store, user=user, date=date,
253 253 branch=None, extra=extra)
254 254
255 255 class filefixupstate(object):
256 256 """state needed to apply fixups to a single file
257 257
258 258 internally, it keeps file contents of several revisions and a linelog.
259 259
260 260 the linelog uses odd revision numbers for original contents (fctxs passed
261 261 to __init__), and even revision numbers for fixups, like:
262 262
263 263 linelog rev 1: self.fctxs[0] (from an immutable "public" changeset)
264 264 linelog rev 2: fixups made to self.fctxs[0]
265 265 linelog rev 3: self.fctxs[1] (a child of fctxs[0])
266 266 linelog rev 4: fixups made to self.fctxs[1]
267 267 ...
268 268
269 269 a typical use is like:
270 270
271 271 1. call diffwith, to calculate self.fixups
272 272 2. (optionally), present self.fixups to the user, or change it
273 273 3. call apply, to apply changes
274 274 4. read results from "finalcontents", or call getfinalcontent
275 275 """
276 276
277 277 def __init__(self, fctxs, ui=None, opts=None):
278 278 """([fctx], ui or None) -> None
279 279
280 280 fctxs should be linear, and sorted by topo order - oldest first.
281 281 fctxs[0] will be considered as "immutable" and will not be changed.
282 282 """
283 283 self.fctxs = fctxs
284 284 self.ui = ui or nullui()
285 285 self.opts = opts or {}
286 286
287 287 # following fields are built from fctxs. they exist for perf reason
288 288 self.contents = [f.data() for f in fctxs]
289 289 self.contentlines = pycompat.maplist(mdiff.splitnewlines, self.contents)
290 290 self.linelog = self._buildlinelog()
291 291 if self.ui.debugflag:
292 292 assert self._checkoutlinelog() == self.contents
293 293
294 294 # following fields will be filled later
295 295 self.chunkstats = [0, 0] # [adopted, total : int]
296 296 self.targetlines = [] # [str]
297 297 self.fixups = [] # [(linelog rev, a1, a2, b1, b2)]
298 298 self.finalcontents = [] # [str]
299 299
300 300 def diffwith(self, targetfctx, showchanges=False):
301 301 """calculate fixups needed by examining the differences between
302 302 self.fctxs[-1] and targetfctx, chunk by chunk.
303 303
304 304 targetfctx is the target state we move towards. we may or may not be
305 305 able to get there because not all modified chunks can be amended into
306 306 a non-public fctx unambiguously.
307 307
308 308 call this only once, before apply().
309 309
310 310 update self.fixups, self.chunkstats, and self.targetlines.
311 311 """
312 312 a = self.contents[-1]
313 313 alines = self.contentlines[-1]
314 314 b = targetfctx.data()
315 315 blines = mdiff.splitnewlines(b)
316 316 self.targetlines = blines
317 317
318 318 self.linelog.annotate(self.linelog.maxrev)
319 319 annotated = self.linelog.annotateresult # [(linelog rev, linenum)]
320 320 assert len(annotated) == len(alines)
321 321 # add a dummy end line to make insertion at the end easier
322 322 if annotated:
323 323 dummyendline = (annotated[-1][0], annotated[-1][1] + 1)
324 324 annotated.append(dummyendline)
325 325
326 326 # analyse diff blocks
327 327 for chunk in self._alldiffchunks(a, b, alines, blines):
328 328 newfixups = self._analysediffchunk(chunk, annotated)
329 329 self.chunkstats[0] += bool(newfixups) # 1 or 0
330 330 self.chunkstats[1] += 1
331 331 self.fixups += newfixups
332 332 if showchanges:
333 333 self._showchanges(alines, blines, chunk, newfixups)
334 334
335 335 def apply(self):
336 336 """apply self.fixups. update self.linelog, self.finalcontents.
337 337
338 338 call this only once, before getfinalcontent(), after diffwith().
339 339 """
340 340 # the following is unnecessary, as it's done by "diffwith":
341 341 # self.linelog.annotate(self.linelog.maxrev)
342 342 for rev, a1, a2, b1, b2 in reversed(self.fixups):
343 343 blines = self.targetlines[b1:b2]
344 344 if self.ui.debugflag:
345 345 idx = (max(rev - 1, 0)) // 2
346 346 self.ui.write(_('%s: chunk %d:%d -> %d lines\n')
347 347 % (node.short(self.fctxs[idx].node()),
348 348 a1, a2, len(blines)))
349 349 self.linelog.replacelines(rev, a1, a2, b1, b2)
350 350 if self.opts.get('edit_lines', False):
351 351 self.finalcontents = self._checkoutlinelogwithedits()
352 352 else:
353 353 self.finalcontents = self._checkoutlinelog()
354 354
355 355 def getfinalcontent(self, fctx):
356 356 """(fctx) -> str. get modified file content for a given filecontext"""
357 357 idx = self.fctxs.index(fctx)
358 358 return self.finalcontents[idx]
359 359
360 360 def _analysediffchunk(self, chunk, annotated):
361 361 """analyse a different chunk and return new fixups found
362 362
363 363 return [] if no lines from the chunk can be safely applied.
364 364
365 365 the chunk (or lines) cannot be safely applied, if, for example:
366 366 - the modified (deleted) lines belong to a public changeset
367 367 (self.fctxs[0])
368 368 - the chunk is a pure insertion and the adjacent lines (at most 2
369 369 lines) belong to different non-public changesets, or do not belong
370 370 to any non-public changesets.
371 371 - the chunk is modifying lines from different changesets.
372 372 in this case, if the number of lines deleted equals to the number
373 373 of lines added, assume it's a simple 1:1 map (could be wrong).
374 374 otherwise, give up.
375 375 - the chunk is modifying lines from a single non-public changeset,
376 376 but other revisions touch the area as well. i.e. the lines are
377 377 not continuous as seen from the linelog.
378 378 """
379 379 a1, a2, b1, b2 = chunk
380 380 # find involved indexes from annotate result
381 381 involved = annotated[a1:a2]
382 382 if not involved and annotated: # a1 == a2 and a is not empty
383 383 # pure insertion, check nearby lines. ignore lines belong
384 384 # to the public (first) changeset (i.e. annotated[i][0] == 1)
385 385 nearbylinenums = {a2, max(0, a1 - 1)}
386 386 involved = [annotated[i]
387 387 for i in nearbylinenums if annotated[i][0] != 1]
388 388 involvedrevs = list(set(r for r, l in involved))
389 389 newfixups = []
390 390 if len(involvedrevs) == 1 and self._iscontinuous(a1, a2 - 1, True):
391 391 # chunk belongs to a single revision
392 392 rev = involvedrevs[0]
393 393 if rev > 1:
394 394 fixuprev = rev + 1
395 395 newfixups.append((fixuprev, a1, a2, b1, b2))
396 396 elif a2 - a1 == b2 - b1 or b1 == b2:
397 397 # 1:1 line mapping, or chunk was deleted
398 398 for i in pycompat.xrange(a1, a2):
399 399 rev, linenum = annotated[i]
400 400 if rev > 1:
401 401 if b1 == b2: # deletion, simply remove that single line
402 402 nb1 = nb2 = 0
403 403 else: # 1:1 line mapping, change the corresponding rev
404 404 nb1 = b1 + i - a1
405 405 nb2 = nb1 + 1
406 406 fixuprev = rev + 1
407 407 newfixups.append((fixuprev, i, i + 1, nb1, nb2))
408 408 return self._optimizefixups(newfixups)
409 409
410 410 @staticmethod
411 411 def _alldiffchunks(a, b, alines, blines):
412 412 """like mdiff.allblocks, but only care about differences"""
413 413 blocks = mdiff.allblocks(a, b, lines1=alines, lines2=blines)
414 414 for chunk, btype in blocks:
415 415 if btype != '!':
416 416 continue
417 417 yield chunk
418 418
419 419 def _buildlinelog(self):
420 420 """calculate the initial linelog based on self.content{,line}s.
421 421 this is similar to running a partial "annotate".
422 422 """
423 423 llog = linelog.linelog()
424 424 a, alines = '', []
425 425 for i in pycompat.xrange(len(self.contents)):
426 426 b, blines = self.contents[i], self.contentlines[i]
427 427 llrev = i * 2 + 1
428 428 chunks = self._alldiffchunks(a, b, alines, blines)
429 429 for a1, a2, b1, b2 in reversed(list(chunks)):
430 430 llog.replacelines(llrev, a1, a2, b1, b2)
431 431 a, alines = b, blines
432 432 return llog
433 433
434 434 def _checkoutlinelog(self):
435 435 """() -> [str]. check out file contents from linelog"""
436 436 contents = []
437 437 for i in pycompat.xrange(len(self.contents)):
438 438 rev = (i + 1) * 2
439 439 self.linelog.annotate(rev)
440 440 content = ''.join(map(self._getline, self.linelog.annotateresult))
441 441 contents.append(content)
442 442 return contents
443 443
444 444 def _checkoutlinelogwithedits(self):
445 445 """() -> [str]. prompt all lines for edit"""
446 446 alllines = self.linelog.getalllines()
447 447 # header
448 448 editortext = (_('HG: editing %s\nHG: "y" means the line to the right '
449 449 'exists in the changeset to the top\nHG:\n')
450 450 % self.fctxs[-1].path())
451 451 # [(idx, fctx)]. hide the dummy emptyfilecontext
452 452 visiblefctxs = [(i, f)
453 453 for i, f in enumerate(self.fctxs)
454 454 if not isinstance(f, emptyfilecontext)]
455 455 for i, (j, f) in enumerate(visiblefctxs):
456 456 editortext += (_('HG: %s/%s %s %s\n') %
457 457 ('|' * i, '-' * (len(visiblefctxs) - i + 1),
458 458 node.short(f.node()),
459 459 f.description().split('\n',1)[0]))
460 460 editortext += _('HG: %s\n') % ('|' * len(visiblefctxs))
461 461 # figure out the lifetime of a line, this is relatively inefficient,
462 462 # but probably fine
463 463 lineset = defaultdict(lambda: set()) # {(llrev, linenum): {llrev}}
464 464 for i, f in visiblefctxs:
465 465 self.linelog.annotate((i + 1) * 2)
466 466 for l in self.linelog.annotateresult:
467 467 lineset[l].add(i)
468 468 # append lines
469 469 for l in alllines:
470 470 editortext += (' %s : %s' %
471 471 (''.join([('y' if i in lineset[l] else ' ')
472 472 for i, _f in visiblefctxs]),
473 473 self._getline(l)))
474 474 # run editor
475 475 editedtext = self.ui.edit(editortext, '', action='absorb')
476 476 if not editedtext:
477 477 raise error.Abort(_('empty editor text'))
478 478 # parse edited result
479 479 contents = ['' for i in self.fctxs]
480 480 leftpadpos = 4
481 481 colonpos = leftpadpos + len(visiblefctxs) + 1
482 482 for l in mdiff.splitnewlines(editedtext):
483 483 if l.startswith('HG:'):
484 484 continue
485 485 if l[colonpos - 1:colonpos + 2] != ' : ':
486 486 raise error.Abort(_('malformed line: %s') % l)
487 487 linecontent = l[colonpos + 2:]
488 488 for i, ch in enumerate(l[leftpadpos:colonpos - 1]):
489 489 if ch == 'y':
490 490 contents[visiblefctxs[i][0]] += linecontent
491 491 # chunkstats is hard to calculate if anything changes, therefore
492 492 # set them to just a simple value (1, 1).
493 493 if editedtext != editortext:
494 494 self.chunkstats = [1, 1]
495 495 return contents
496 496
497 497 def _getline(self, lineinfo):
498 498 """((rev, linenum)) -> str. convert rev+line number to line content"""
499 499 rev, linenum = lineinfo
500 500 if rev & 1: # odd: original line taken from fctxs
501 501 return self.contentlines[rev // 2][linenum]
502 502 else: # even: fixup line from targetfctx
503 503 return self.targetlines[linenum]
504 504
505 505 def _iscontinuous(self, a1, a2, closedinterval=False):
506 506 """(a1, a2 : int) -> bool
507 507
508 508 check if these lines are continuous. i.e. no other insertions or
509 509 deletions (from other revisions) among these lines.
510 510
511 511 closedinterval decides whether a2 should be included or not. i.e. is
512 512 it [a1, a2), or [a1, a2] ?
513 513 """
514 514 if a1 >= a2:
515 515 return True
516 516 llog = self.linelog
517 517 offset1 = llog.getoffset(a1)
518 518 offset2 = llog.getoffset(a2) + int(closedinterval)
519 519 linesinbetween = llog.getalllines(offset1, offset2)
520 520 return len(linesinbetween) == a2 - a1 + int(closedinterval)
521 521
522 522 def _optimizefixups(self, fixups):
523 523 """[(rev, a1, a2, b1, b2)] -> [(rev, a1, a2, b1, b2)].
524 524 merge adjacent fixups to make them less fragmented.
525 525 """
526 526 result = []
527 527 pcurrentchunk = [[-1, -1, -1, -1, -1]]
528 528
529 529 def pushchunk():
530 530 if pcurrentchunk[0][0] != -1:
531 531 result.append(tuple(pcurrentchunk[0]))
532 532
533 533 for i, chunk in enumerate(fixups):
534 534 rev, a1, a2, b1, b2 = chunk
535 535 lastrev = pcurrentchunk[0][0]
536 536 lasta2 = pcurrentchunk[0][2]
537 537 lastb2 = pcurrentchunk[0][4]
538 538 if (a1 == lasta2 and b1 == lastb2 and rev == lastrev and
539 539 self._iscontinuous(max(a1 - 1, 0), a1)):
540 540 # merge into currentchunk
541 541 pcurrentchunk[0][2] = a2
542 542 pcurrentchunk[0][4] = b2
543 543 else:
544 544 pushchunk()
545 545 pcurrentchunk[0] = list(chunk)
546 546 pushchunk()
547 547 return result
548 548
549 549 def _showchanges(self, alines, blines, chunk, fixups):
550 550 ui = self.ui
551 551
552 552 def label(line, label):
553 553 if line.endswith('\n'):
554 554 line = line[:-1]
555 555 return ui.label(line, label)
556 556
557 557 # this is not optimized for perf but _showchanges only gets executed
558 558 # with an extra command-line flag.
559 559 a1, a2, b1, b2 = chunk
560 560 aidxs, bidxs = [0] * (a2 - a1), [0] * (b2 - b1)
561 561 for idx, fa1, fa2, fb1, fb2 in fixups:
562 562 for i in pycompat.xrange(fa1, fa2):
563 563 aidxs[i - a1] = (max(idx, 1) - 1) // 2
564 564 for i in pycompat.xrange(fb1, fb2):
565 565 bidxs[i - b1] = (max(idx, 1) - 1) // 2
566 566
567 567 buf = [] # [(idx, content)]
568 568 buf.append((0, label('@@ -%d,%d +%d,%d @@'
569 569 % (a1, a2 - a1, b1, b2 - b1), 'diff.hunk')))
570 570 buf += [(aidxs[i - a1], label('-' + alines[i], 'diff.deleted'))
571 571 for i in pycompat.xrange(a1, a2)]
572 572 buf += [(bidxs[i - b1], label('+' + blines[i], 'diff.inserted'))
573 573 for i in pycompat.xrange(b1, b2)]
574 574 for idx, line in buf:
575 575 shortnode = idx and node.short(self.fctxs[idx].node()) or ''
576 576 ui.write(ui.label(shortnode[0:7].ljust(8), 'absorb.node') +
577 577 line + '\n')
578 578
579 579 class fixupstate(object):
580 580 """state needed to run absorb
581 581
582 582 internally, it keeps paths and filefixupstates.
583 583
584 584 a typical use is like filefixupstates:
585 585
586 586 1. call diffwith, to calculate fixups
587 587 2. (optionally), present fixups to the user, or edit fixups
588 588 3. call apply, to apply changes to memory
589 589 4. call commit, to commit changes to hg database
590 590 """
591 591
592 592 def __init__(self, stack, ui=None, opts=None):
593 593 """([ctx], ui or None) -> None
594 594
595 595 stack: should be linear, and sorted by topo order - oldest first.
596 596 all commits in stack are considered mutable.
597 597 """
598 598 assert stack
599 599 self.ui = ui or nullui()
600 600 self.opts = opts or {}
601 601 self.stack = stack
602 602 self.repo = stack[-1].repo().unfiltered()
603 603
604 604 # following fields will be filled later
605 605 self.paths = [] # [str]
606 606 self.status = None # ctx.status output
607 607 self.fctxmap = {} # {path: {ctx: fctx}}
608 608 self.fixupmap = {} # {path: filefixupstate}
609 609 self.replacemap = {} # {oldnode: newnode or None}
610 610 self.finalnode = None # head after all fixups
611 611
612 612 def diffwith(self, targetctx, match=None, showchanges=False):
613 613 """diff and prepare fixups. update self.fixupmap, self.paths"""
614 614 # only care about modified files
615 615 self.status = self.stack[-1].status(targetctx, match)
616 616 self.paths = []
617 617 # but if --edit-lines is used, the user may want to edit files
618 618 # even if they are not modified
619 619 editopt = self.opts.get('edit_lines')
620 620 if not self.status.modified and editopt and match:
621 621 interestingpaths = match.files()
622 622 else:
623 623 interestingpaths = self.status.modified
624 624 # prepare the filefixupstate
625 625 seenfctxs = set()
626 626 # sorting is necessary to eliminate ambiguity for the "double move"
627 627 # case: "hg cp A B; hg cp A C; hg rm A", then only "B" can affect "A".
628 628 for path in sorted(interestingpaths):
629 629 self.ui.debug('calculating fixups for %s\n' % path)
630 630 targetfctx = targetctx[path]
631 631 fctxs, ctx2fctx = getfilestack(self.stack, path, seenfctxs)
632 632 # ignore symbolic links or binary, or unchanged files
633 633 if any(f.islink() or stringutil.binary(f.data())
634 634 for f in [targetfctx] + fctxs
635 635 if not isinstance(f, emptyfilecontext)):
636 636 continue
637 637 if targetfctx.data() == fctxs[-1].data() and not editopt:
638 638 continue
639 639 seenfctxs.update(fctxs[1:])
640 640 self.fctxmap[path] = ctx2fctx
641 641 fstate = filefixupstate(fctxs, ui=self.ui, opts=self.opts)
642 642 if showchanges:
643 643 colorpath = self.ui.label(path, 'absorb.path')
644 644 header = 'showing changes for ' + colorpath
645 645 self.ui.write(header + '\n')
646 646 fstate.diffwith(targetfctx, showchanges=showchanges)
647 647 self.fixupmap[path] = fstate
648 648 self.paths.append(path)
649 649
650 650 def apply(self):
651 651 """apply fixups to individual filefixupstates"""
652 652 for path, state in self.fixupmap.iteritems():
653 653 if self.ui.debugflag:
654 654 self.ui.write(_('applying fixups to %s\n') % path)
655 655 state.apply()
656 656
657 657 @property
658 658 def chunkstats(self):
659 659 """-> {path: chunkstats}. collect chunkstats from filefixupstates"""
660 660 return dict((path, state.chunkstats)
661 661 for path, state in self.fixupmap.iteritems())
662 662
663 663 def commit(self):
664 664 """commit changes. update self.finalnode, self.replacemap"""
665 665 with self.repo.wlock(), self.repo.lock():
666 666 with self.repo.transaction('absorb') as tr:
667 667 self._commitstack()
668 668 self._movebookmarks(tr)
669 669 if self.repo['.'].node() in self.replacemap:
670 670 self._moveworkingdirectoryparent()
671 671 if self._useobsolete:
672 672 self._obsoleteoldcommits()
673 673 if not self._useobsolete: # strip must be outside transactions
674 674 self._stripoldcommits()
675 675 return self.finalnode
676 676
677 677 def printchunkstats(self):
678 678 """print things like '1 of 2 chunk(s) applied'"""
679 679 ui = self.ui
680 680 chunkstats = self.chunkstats
681 681 if ui.verbose:
682 682 # chunkstats for each file
683 683 for path, stat in chunkstats.iteritems():
684 684 if stat[0]:
685 685 ui.write(_('%s: %d of %d chunk(s) applied\n')
686 686 % (path, stat[0], stat[1]))
687 687 elif not ui.quiet:
688 688 # a summary for all files
689 689 stats = chunkstats.values()
690 690 applied, total = (sum(s[i] for s in stats) for i in (0, 1))
691 691 ui.write(_('%d of %d chunk(s) applied\n') % (applied, total))
692 692
693 693 def _commitstack(self):
694 694 """make new commits. update self.finalnode, self.replacemap.
695 695 it is splitted from "commit" to avoid too much indentation.
696 696 """
697 697 # last node (20-char) committed by us
698 698 lastcommitted = None
699 699 # p1 which overrides the parent of the next commit, "None" means use
700 700 # the original parent unchanged
701 701 nextp1 = None
702 702 for ctx in self.stack:
703 703 memworkingcopy = self._getnewfilecontents(ctx)
704 704 if not memworkingcopy and not lastcommitted:
705 705 # nothing changed, nothing commited
706 706 nextp1 = ctx
707 707 continue
708 708 msg = ''
709 709 if self._willbecomenoop(memworkingcopy, ctx, nextp1):
710 710 # changeset is no longer necessary
711 711 self.replacemap[ctx.node()] = None
712 712 msg = _('became empty and was dropped')
713 713 else:
714 714 # changeset needs re-commit
715 715 nodestr = self._commitsingle(memworkingcopy, ctx, p1=nextp1)
716 716 lastcommitted = self.repo[nodestr]
717 717 nextp1 = lastcommitted
718 718 self.replacemap[ctx.node()] = lastcommitted.node()
719 719 if memworkingcopy:
720 720 msg = _('%d file(s) changed, became %s') % (
721 721 len(memworkingcopy), self._ctx2str(lastcommitted))
722 722 else:
723 723 msg = _('became %s') % self._ctx2str(lastcommitted)
724 724 if self.ui.verbose and msg:
725 725 self.ui.write(_('%s: %s\n') % (self._ctx2str(ctx), msg))
726 726 self.finalnode = lastcommitted and lastcommitted.node()
727 727
728 728 def _ctx2str(self, ctx):
729 729 if self.ui.debugflag:
730 730 return ctx.hex()
731 731 else:
732 732 return node.short(ctx.node())
733 733
734 734 def _getnewfilecontents(self, ctx):
735 735 """(ctx) -> {path: str}
736 736
737 737 fetch file contents from filefixupstates.
738 738 return the working copy overrides - files different from ctx.
739 739 """
740 740 result = {}
741 741 for path in self.paths:
742 742 ctx2fctx = self.fctxmap[path] # {ctx: fctx}
743 743 if ctx not in ctx2fctx:
744 744 continue
745 745 fctx = ctx2fctx[ctx]
746 746 content = fctx.data()
747 747 newcontent = self.fixupmap[path].getfinalcontent(fctx)
748 748 if content != newcontent:
749 749 result[fctx.path()] = newcontent
750 750 return result
751 751
752 752 def _movebookmarks(self, tr):
753 753 repo = self.repo
754 754 needupdate = [(name, self.replacemap[hsh])
755 755 for name, hsh in repo._bookmarks.iteritems()
756 756 if hsh in self.replacemap]
757 757 changes = []
758 758 for name, hsh in needupdate:
759 759 if hsh:
760 760 changes.append((name, hsh))
761 761 if self.ui.verbose:
762 762 self.ui.write(_('moving bookmark %s to %s\n')
763 763 % (name, node.hex(hsh)))
764 764 else:
765 765 changes.append((name, None))
766 766 if self.ui.verbose:
767 767 self.ui.write(_('deleting bookmark %s\n') % name)
768 768 repo._bookmarks.applychanges(repo, tr, changes)
769 769
770 770 def _moveworkingdirectoryparent(self):
771 771 if not self.finalnode:
772 772 # Find the latest not-{obsoleted,stripped} parent.
773 773 revs = self.repo.revs('max(::. - %ln)', self.replacemap.keys())
774 774 ctx = self.repo[revs.first()]
775 775 self.finalnode = ctx.node()
776 776 else:
777 777 ctx = self.repo[self.finalnode]
778 778
779 779 dirstate = self.repo.dirstate
780 780 # dirstate.rebuild invalidates fsmonitorstate, causing "hg status" to
781 781 # be slow. in absorb's case, no need to invalidate fsmonitorstate.
782 782 noop = lambda: 0
783 783 restore = noop
784 784 if util.safehasattr(dirstate, '_fsmonitorstate'):
785 785 bak = dirstate._fsmonitorstate.invalidate
786 786 def restore():
787 787 dirstate._fsmonitorstate.invalidate = bak
788 788 dirstate._fsmonitorstate.invalidate = noop
789 789 try:
790 790 with dirstate.parentchange():
791 791 dirstate.rebuild(ctx.node(), ctx.manifest(), self.paths)
792 792 finally:
793 793 restore()
794 794
795 795 @staticmethod
796 796 def _willbecomenoop(memworkingcopy, ctx, pctx=None):
797 797 """({path: content}, ctx, ctx) -> bool. test if a commit will be noop
798 798
799 799 if it will become an empty commit (does not change anything, after the
800 800 memworkingcopy overrides), return True. otherwise return False.
801 801 """
802 802 if not pctx:
803 803 parents = ctx.parents()
804 804 if len(parents) != 1:
805 805 return False
806 806 pctx = parents[0]
807 807 # ctx changes more files (not a subset of memworkingcopy)
808 808 if not set(ctx.files()).issubset(set(memworkingcopy)):
809 809 return False
810 810 for path, content in memworkingcopy.iteritems():
811 811 if path not in pctx or path not in ctx:
812 812 return False
813 813 fctx = ctx[path]
814 814 pfctx = pctx[path]
815 815 if pfctx.flags() != fctx.flags():
816 816 return False
817 817 if pfctx.data() != content:
818 818 return False
819 819 return True
820 820
821 821 def _commitsingle(self, memworkingcopy, ctx, p1=None):
822 822 """(ctx, {path: content}, node) -> node. make a single commit
823 823
824 824 the commit is a clone from ctx, with a (optionally) different p1, and
825 825 different file contents replaced by memworkingcopy.
826 826 """
827 827 parents = p1 and (p1, node.nullid)
828 828 extra = ctx.extra()
829 829 if self._useobsolete and self.ui.configbool('absorb', 'add-noise'):
830 830 extra['absorb_source'] = ctx.hex()
831 831 mctx = overlaycontext(memworkingcopy, ctx, parents, extra=extra)
832 832 # preserve phase
833 833 with mctx.repo().ui.configoverride({
834 834 ('phases', 'new-commit'): ctx.phase()}):
835 835 return mctx.commit()
836 836
837 837 @util.propertycache
838 838 def _useobsolete(self):
839 839 """() -> bool"""
840 840 return obsolete.isenabled(self.repo, obsolete.createmarkersopt)
841 841
842 842 def _obsoleteoldcommits(self):
843 843 relations = [(self.repo[k], v and (self.repo[v],) or ())
844 844 for k, v in self.replacemap.iteritems()]
845 845 if relations:
846 846 obsolete.createmarkers(self.repo, relations)
847 847
848 848 def _stripoldcommits(self):
849 849 nodelist = self.replacemap.keys()
850 850 # make sure we don't strip innocent children
851 851 revs = self.repo.revs('%ln - (::(heads(%ln::)-%ln))', nodelist,
852 852 nodelist, nodelist)
853 853 tonode = self.repo.changelog.node
854 854 nodelist = [tonode(r) for r in revs]
855 855 if nodelist:
856 856 repair.strip(self.repo.ui, self.repo, nodelist)
857 857
858 858 def _parsechunk(hunk):
859 859 """(crecord.uihunk or patch.recordhunk) -> (path, (a1, a2, [bline]))"""
860 860 if type(hunk) not in (crecord.uihunk, patch.recordhunk):
861 861 return None, None
862 862 path = hunk.header.filename()
863 863 a1 = hunk.fromline + len(hunk.before) - 1
864 864 # remove before and after context
865 865 hunk.before = hunk.after = []
866 866 buf = util.stringio()
867 867 hunk.write(buf)
868 868 patchlines = mdiff.splitnewlines(buf.getvalue())
869 869 # hunk.prettystr() will update hunk.removed
870 870 a2 = a1 + hunk.removed
871 871 blines = [l[1:] for l in patchlines[1:] if l[0] != '-']
872 872 return path, (a1, a2, blines)
873 873
874 874 def overlaydiffcontext(ctx, chunks):
875 875 """(ctx, [crecord.uihunk]) -> memctx
876 876
877 877 return a memctx with some [1] patches (chunks) applied to ctx.
878 878 [1]: modifications are handled. renames, mode changes, etc. are ignored.
879 879 """
880 880 # sadly the applying-patch logic is hardly reusable, and messy:
881 881 # 1. the core logic "_applydiff" is too heavy - it writes .rej files, it
882 882 # needs a file stream of a patch and will re-parse it, while we have
883 883 # structured hunk objects at hand.
884 884 # 2. a lot of different implementations about "chunk" (patch.hunk,
885 885 # patch.recordhunk, crecord.uihunk)
886 886 # as we only care about applying changes to modified files, no mode
887 887 # change, no binary diff, and no renames, it's probably okay to
888 888 # re-invent the logic using much simpler code here.
889 889 memworkingcopy = {} # {path: content}
890 890 patchmap = defaultdict(lambda: []) # {path: [(a1, a2, [bline])]}
891 891 for path, info in map(_parsechunk, chunks):
892 892 if not path or not info:
893 893 continue
894 894 patchmap[path].append(info)
895 895 for path, patches in patchmap.iteritems():
896 896 if path not in ctx or not patches:
897 897 continue
898 898 patches.sort(reverse=True)
899 899 lines = mdiff.splitnewlines(ctx[path].data())
900 900 for a1, a2, blines in patches:
901 901 lines[a1:a2] = blines
902 902 memworkingcopy[path] = ''.join(lines)
903 903 return overlaycontext(memworkingcopy, ctx)
904 904
905 905 def absorb(ui, repo, stack=None, targetctx=None, pats=None, opts=None):
906 906 """pick fixup chunks from targetctx, apply them to stack.
907 907
908 908 if targetctx is None, the working copy context will be used.
909 909 if stack is None, the current draft stack will be used.
910 910 return fixupstate.
911 911 """
912 912 if stack is None:
913 913 limit = ui.configint('absorb', 'max-stack-size')
914 914 stack = getdraftstack(repo['.'], limit)
915 915 if limit and len(stack) >= limit:
916 916 ui.warn(_('absorb: only the recent %d changesets will '
917 917 'be analysed\n')
918 918 % limit)
919 919 if not stack:
920 920 raise error.Abort(_('no mutable changeset to change'))
921 921 if targetctx is None: # default to working copy
922 922 targetctx = repo[None]
923 923 if pats is None:
924 924 pats = ()
925 925 if opts is None:
926 926 opts = {}
927 927 state = fixupstate(stack, ui=ui, opts=opts)
928 928 matcher = scmutil.match(targetctx, pats, opts)
929 929 if opts.get('interactive'):
930 930 diff = patch.diff(repo, stack[-1].node(), targetctx.node(), matcher)
931 931 origchunks = patch.parsepatch(diff)
932 932 chunks = cmdutil.recordfilter(ui, origchunks)[0]
933 933 targetctx = overlaydiffcontext(stack[-1], chunks)
934 934 state.diffwith(targetctx, matcher, showchanges=opts.get('print_changes'))
935 935 if not opts.get('dry_run'):
936 936 state.apply()
937 937 if state.commit():
938 938 state.printchunkstats()
939 939 elif not ui.quiet:
940 940 ui.write(_('nothing applied\n'))
941 941 return state
942 942
943 943 @command('^absorb',
944 944 [('p', 'print-changes', None,
945 945 _('print which changesets are modified by which changes')),
946 946 ('i', 'interactive', None,
947 947 _('interactively select which chunks to apply (EXPERIMENTAL)')),
948 948 ('e', 'edit-lines', None,
949 949 _('edit what lines belong to which changesets before commit '
950 950 '(EXPERIMENTAL)')),
951 951 ] + commands.dryrunopts + commands.walkopts,
952 952 _('hg absorb [OPTION] [FILE]...'))
953 953 def absorbcmd(ui, repo, *pats, **opts):
954 954 """incorporate corrections into the stack of draft changesets
955 955
956 956 absorb analyzes each change in your working directory and attempts to
957 957 amend the changed lines into the changesets in your stack that first
958 958 introduced those lines.
959 959
960 960 If absorb cannot find an unambiguous changeset to amend for a change,
961 961 that change will be left in the working directory, untouched. They can be
962 962 observed by :hg:`status` or :hg:`diff` afterwards. In other words,
963 963 absorb does not write to the working directory.
964 964
965 965 Changesets outside the revset `::. and not public() and not merge()` will
966 966 not be changed.
967 967
968 968 Changesets that become empty after applying the changes will be deleted.
969 969
970 970 If in doubt, run :hg:`absorb -pn` to preview what changesets will
971 971 be amended by what changed lines, without actually changing anything.
972 972
973 973 Returns 0 on success, 1 if all chunks were ignored and nothing amended.
974 974 """
975 opts = pycompat.byteskwargs(opts)
975 976 state = absorb(ui, repo, pats=pats, opts=opts)
976 977 if sum(s[0] for s in state.chunkstats.values()) == 0:
977 978 return 1
General Comments 0
You need to be logged in to leave comments. Login now