##// END OF EJS Templates
help: assigning categories to existing commands...
rdamazio@google.com -
r40329:c303d65d default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,1024 +1,1025 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.description = yellow
25 25 absorb.node = blue bold
26 26 absorb.path = bold
27 27 """
28 28
29 29 # TODO:
30 30 # * Rename config items to [commands] namespace
31 31 # * Converge getdraftstack() with other code in core
32 32 # * move many attributes on fixupstate to be private
33 33
34 34 from __future__ import absolute_import
35 35
36 36 import collections
37 37
38 38 from mercurial.i18n import _
39 39 from mercurial import (
40 40 cmdutil,
41 41 commands,
42 42 context,
43 43 crecord,
44 44 error,
45 45 linelog,
46 46 mdiff,
47 47 node,
48 48 obsolete,
49 49 patch,
50 50 phases,
51 51 pycompat,
52 52 registrar,
53 53 repair,
54 54 scmutil,
55 55 util,
56 56 )
57 57 from mercurial.utils import (
58 58 stringutil,
59 59 )
60 60
61 61 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
62 62 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
63 63 # be specifying the version(s) of Mercurial they are tested with, or
64 64 # leave the attribute unspecified.
65 65 testedwith = 'ships-with-hg-core'
66 66
67 67 cmdtable = {}
68 68 command = registrar.command(cmdtable)
69 69
70 70 configtable = {}
71 71 configitem = registrar.configitem(configtable)
72 72
73 73 configitem('absorb', 'add-noise', default=True)
74 74 configitem('absorb', 'amend-flag', default=None)
75 75 configitem('absorb', 'max-stack-size', default=50)
76 76
77 77 colortable = {
78 78 'absorb.description': 'yellow',
79 79 'absorb.node': 'blue bold',
80 80 'absorb.path': 'bold',
81 81 }
82 82
83 83 defaultdict = collections.defaultdict
84 84
85 85 class nullui(object):
86 86 """blank ui object doing nothing"""
87 87 debugflag = False
88 88 verbose = False
89 89 quiet = True
90 90
91 91 def __getitem__(name):
92 92 def nullfunc(*args, **kwds):
93 93 return
94 94 return nullfunc
95 95
96 96 class emptyfilecontext(object):
97 97 """minimal filecontext representing an empty file"""
98 98 def data(self):
99 99 return ''
100 100
101 101 def node(self):
102 102 return node.nullid
103 103
104 104 def uniq(lst):
105 105 """list -> list. remove duplicated items without changing the order"""
106 106 seen = set()
107 107 result = []
108 108 for x in lst:
109 109 if x not in seen:
110 110 seen.add(x)
111 111 result.append(x)
112 112 return result
113 113
114 114 def getdraftstack(headctx, limit=None):
115 115 """(ctx, int?) -> [ctx]. get a linear stack of non-public changesets.
116 116
117 117 changesets are sorted in topo order, oldest first.
118 118 return at most limit items, if limit is a positive number.
119 119
120 120 merges are considered as non-draft as well. i.e. every commit
121 121 returned has and only has 1 parent.
122 122 """
123 123 ctx = headctx
124 124 result = []
125 125 while ctx.phase() != phases.public:
126 126 if limit and len(result) >= limit:
127 127 break
128 128 parents = ctx.parents()
129 129 if len(parents) != 1:
130 130 break
131 131 result.append(ctx)
132 132 ctx = parents[0]
133 133 result.reverse()
134 134 return result
135 135
136 136 def getfilestack(stack, path, seenfctxs=None):
137 137 """([ctx], str, set) -> [fctx], {ctx: fctx}
138 138
139 139 stack is a list of contexts, from old to new. usually they are what
140 140 "getdraftstack" returns.
141 141
142 142 follows renames, but not copies.
143 143
144 144 seenfctxs is a set of filecontexts that will be considered "immutable".
145 145 they are usually what this function returned in earlier calls, useful
146 146 to avoid issues that a file was "moved" to multiple places and was then
147 147 modified differently, like: "a" was copied to "b", "a" was also copied to
148 148 "c" and then "a" was deleted, then both "b" and "c" were "moved" from "a"
149 149 and we enforce only one of them to be able to affect "a"'s content.
150 150
151 151 return an empty list and an empty dict, if the specified path does not
152 152 exist in stack[-1] (the top of the stack).
153 153
154 154 otherwise, return a list of de-duplicated filecontexts, and the map to
155 155 convert ctx in the stack to fctx, for possible mutable fctxs. the first item
156 156 of the list would be outside the stack and should be considered immutable.
157 157 the remaining items are within the stack.
158 158
159 159 for example, given the following changelog and corresponding filelog
160 160 revisions:
161 161
162 162 changelog: 3----4----5----6----7
163 163 filelog: x 0----1----1----2 (x: no such file yet)
164 164
165 165 - if stack = [5, 6, 7], returns ([0, 1, 2], {5: 1, 6: 1, 7: 2})
166 166 - if stack = [3, 4, 5], returns ([e, 0, 1], {4: 0, 5: 1}), where "e" is a
167 167 dummy empty filecontext.
168 168 - if stack = [2], returns ([], {})
169 169 - if stack = [7], returns ([1, 2], {7: 2})
170 170 - if stack = [6, 7], returns ([1, 2], {6: 1, 7: 2}), although {6: 1} can be
171 171 removed, since 1 is immutable.
172 172 """
173 173 if seenfctxs is None:
174 174 seenfctxs = set()
175 175 assert stack
176 176
177 177 if path not in stack[-1]:
178 178 return [], {}
179 179
180 180 fctxs = []
181 181 fctxmap = {}
182 182
183 183 pctx = stack[0].p1() # the public (immutable) ctx we stop at
184 184 for ctx in reversed(stack):
185 185 if path not in ctx: # the file is added in the next commit
186 186 pctx = ctx
187 187 break
188 188 fctx = ctx[path]
189 189 fctxs.append(fctx)
190 190 if fctx in seenfctxs: # treat fctx as the immutable one
191 191 pctx = None # do not add another immutable fctx
192 192 break
193 193 fctxmap[ctx] = fctx # only for mutable fctxs
194 194 renamed = fctx.renamed()
195 195 if renamed:
196 196 path = renamed[0] # follow rename
197 197 if path in ctx: # but do not follow copy
198 198 pctx = ctx.p1()
199 199 break
200 200
201 201 if pctx is not None: # need an extra immutable fctx
202 202 if path in pctx:
203 203 fctxs.append(pctx[path])
204 204 else:
205 205 fctxs.append(emptyfilecontext())
206 206
207 207 fctxs.reverse()
208 208 # note: we rely on a property of hg: filerev is not reused for linear
209 209 # history. i.e. it's impossible to have:
210 210 # changelog: 4----5----6 (linear, no merges)
211 211 # filelog: 1----2----1
212 212 # ^ reuse filerev (impossible)
213 213 # because parents are part of the hash. if that's not true, we need to
214 214 # remove uniq and find a different way to identify fctxs.
215 215 return uniq(fctxs), fctxmap
216 216
217 217 class overlaystore(patch.filestore):
218 218 """read-only, hybrid store based on a dict and ctx.
219 219 memworkingcopy: {path: content}, overrides file contents.
220 220 """
221 221 def __init__(self, basectx, memworkingcopy):
222 222 self.basectx = basectx
223 223 self.memworkingcopy = memworkingcopy
224 224
225 225 def getfile(self, path):
226 226 """comply with mercurial.patch.filestore.getfile"""
227 227 if path not in self.basectx:
228 228 return None, None, None
229 229 fctx = self.basectx[path]
230 230 if path in self.memworkingcopy:
231 231 content = self.memworkingcopy[path]
232 232 else:
233 233 content = fctx.data()
234 234 mode = (fctx.islink(), fctx.isexec())
235 235 renamed = fctx.renamed() # False or (path, node)
236 236 return content, mode, (renamed and renamed[0])
237 237
238 238 def overlaycontext(memworkingcopy, ctx, parents=None, extra=None):
239 239 """({path: content}, ctx, (p1node, p2node)?, {}?) -> memctx
240 240 memworkingcopy overrides file contents.
241 241 """
242 242 # parents must contain 2 items: (node1, node2)
243 243 if parents is None:
244 244 parents = ctx.repo().changelog.parents(ctx.node())
245 245 if extra is None:
246 246 extra = ctx.extra()
247 247 date = ctx.date()
248 248 desc = ctx.description()
249 249 user = ctx.user()
250 250 files = set(ctx.files()).union(memworkingcopy)
251 251 store = overlaystore(ctx, memworkingcopy)
252 252 return context.memctx(
253 253 repo=ctx.repo(), parents=parents, text=desc,
254 254 files=files, filectxfn=store, user=user, date=date,
255 255 branch=None, extra=extra)
256 256
257 257 class filefixupstate(object):
258 258 """state needed to apply fixups to a single file
259 259
260 260 internally, it keeps file contents of several revisions and a linelog.
261 261
262 262 the linelog uses odd revision numbers for original contents (fctxs passed
263 263 to __init__), and even revision numbers for fixups, like:
264 264
265 265 linelog rev 1: self.fctxs[0] (from an immutable "public" changeset)
266 266 linelog rev 2: fixups made to self.fctxs[0]
267 267 linelog rev 3: self.fctxs[1] (a child of fctxs[0])
268 268 linelog rev 4: fixups made to self.fctxs[1]
269 269 ...
270 270
271 271 a typical use is like:
272 272
273 273 1. call diffwith, to calculate self.fixups
274 274 2. (optionally), present self.fixups to the user, or change it
275 275 3. call apply, to apply changes
276 276 4. read results from "finalcontents", or call getfinalcontent
277 277 """
278 278
279 279 def __init__(self, fctxs, path, ui=None, opts=None):
280 280 """([fctx], ui or None) -> None
281 281
282 282 fctxs should be linear, and sorted by topo order - oldest first.
283 283 fctxs[0] will be considered as "immutable" and will not be changed.
284 284 """
285 285 self.fctxs = fctxs
286 286 self.path = path
287 287 self.ui = ui or nullui()
288 288 self.opts = opts or {}
289 289
290 290 # following fields are built from fctxs. they exist for perf reason
291 291 self.contents = [f.data() for f in fctxs]
292 292 self.contentlines = pycompat.maplist(mdiff.splitnewlines, self.contents)
293 293 self.linelog = self._buildlinelog()
294 294 if self.ui.debugflag:
295 295 assert self._checkoutlinelog() == self.contents
296 296
297 297 # following fields will be filled later
298 298 self.chunkstats = [0, 0] # [adopted, total : int]
299 299 self.targetlines = [] # [str]
300 300 self.fixups = [] # [(linelog rev, a1, a2, b1, b2)]
301 301 self.finalcontents = [] # [str]
302 302 self.ctxaffected = set()
303 303
304 304 def diffwith(self, targetfctx, fm=None):
305 305 """calculate fixups needed by examining the differences between
306 306 self.fctxs[-1] and targetfctx, chunk by chunk.
307 307
308 308 targetfctx is the target state we move towards. we may or may not be
309 309 able to get there because not all modified chunks can be amended into
310 310 a non-public fctx unambiguously.
311 311
312 312 call this only once, before apply().
313 313
314 314 update self.fixups, self.chunkstats, and self.targetlines.
315 315 """
316 316 a = self.contents[-1]
317 317 alines = self.contentlines[-1]
318 318 b = targetfctx.data()
319 319 blines = mdiff.splitnewlines(b)
320 320 self.targetlines = blines
321 321
322 322 self.linelog.annotate(self.linelog.maxrev)
323 323 annotated = self.linelog.annotateresult # [(linelog rev, linenum)]
324 324 assert len(annotated) == len(alines)
325 325 # add a dummy end line to make insertion at the end easier
326 326 if annotated:
327 327 dummyendline = (annotated[-1][0], annotated[-1][1] + 1)
328 328 annotated.append(dummyendline)
329 329
330 330 # analyse diff blocks
331 331 for chunk in self._alldiffchunks(a, b, alines, blines):
332 332 newfixups = self._analysediffchunk(chunk, annotated)
333 333 self.chunkstats[0] += bool(newfixups) # 1 or 0
334 334 self.chunkstats[1] += 1
335 335 self.fixups += newfixups
336 336 if fm is not None:
337 337 self._showchanges(fm, alines, blines, chunk, newfixups)
338 338
339 339 def apply(self):
340 340 """apply self.fixups. update self.linelog, self.finalcontents.
341 341
342 342 call this only once, before getfinalcontent(), after diffwith().
343 343 """
344 344 # the following is unnecessary, as it's done by "diffwith":
345 345 # self.linelog.annotate(self.linelog.maxrev)
346 346 for rev, a1, a2, b1, b2 in reversed(self.fixups):
347 347 blines = self.targetlines[b1:b2]
348 348 if self.ui.debugflag:
349 349 idx = (max(rev - 1, 0)) // 2
350 350 self.ui.write(_('%s: chunk %d:%d -> %d lines\n')
351 351 % (node.short(self.fctxs[idx].node()),
352 352 a1, a2, len(blines)))
353 353 self.linelog.replacelines(rev, a1, a2, b1, b2)
354 354 if self.opts.get('edit_lines', False):
355 355 self.finalcontents = self._checkoutlinelogwithedits()
356 356 else:
357 357 self.finalcontents = self._checkoutlinelog()
358 358
359 359 def getfinalcontent(self, fctx):
360 360 """(fctx) -> str. get modified file content for a given filecontext"""
361 361 idx = self.fctxs.index(fctx)
362 362 return self.finalcontents[idx]
363 363
364 364 def _analysediffchunk(self, chunk, annotated):
365 365 """analyse a different chunk and return new fixups found
366 366
367 367 return [] if no lines from the chunk can be safely applied.
368 368
369 369 the chunk (or lines) cannot be safely applied, if, for example:
370 370 - the modified (deleted) lines belong to a public changeset
371 371 (self.fctxs[0])
372 372 - the chunk is a pure insertion and the adjacent lines (at most 2
373 373 lines) belong to different non-public changesets, or do not belong
374 374 to any non-public changesets.
375 375 - the chunk is modifying lines from different changesets.
376 376 in this case, if the number of lines deleted equals to the number
377 377 of lines added, assume it's a simple 1:1 map (could be wrong).
378 378 otherwise, give up.
379 379 - the chunk is modifying lines from a single non-public changeset,
380 380 but other revisions touch the area as well. i.e. the lines are
381 381 not continuous as seen from the linelog.
382 382 """
383 383 a1, a2, b1, b2 = chunk
384 384 # find involved indexes from annotate result
385 385 involved = annotated[a1:a2]
386 386 if not involved and annotated: # a1 == a2 and a is not empty
387 387 # pure insertion, check nearby lines. ignore lines belong
388 388 # to the public (first) changeset (i.e. annotated[i][0] == 1)
389 389 nearbylinenums = {a2, max(0, a1 - 1)}
390 390 involved = [annotated[i]
391 391 for i in nearbylinenums if annotated[i][0] != 1]
392 392 involvedrevs = list(set(r for r, l in involved))
393 393 newfixups = []
394 394 if len(involvedrevs) == 1 and self._iscontinuous(a1, a2 - 1, True):
395 395 # chunk belongs to a single revision
396 396 rev = involvedrevs[0]
397 397 if rev > 1:
398 398 fixuprev = rev + 1
399 399 newfixups.append((fixuprev, a1, a2, b1, b2))
400 400 elif a2 - a1 == b2 - b1 or b1 == b2:
401 401 # 1:1 line mapping, or chunk was deleted
402 402 for i in pycompat.xrange(a1, a2):
403 403 rev, linenum = annotated[i]
404 404 if rev > 1:
405 405 if b1 == b2: # deletion, simply remove that single line
406 406 nb1 = nb2 = 0
407 407 else: # 1:1 line mapping, change the corresponding rev
408 408 nb1 = b1 + i - a1
409 409 nb2 = nb1 + 1
410 410 fixuprev = rev + 1
411 411 newfixups.append((fixuprev, i, i + 1, nb1, nb2))
412 412 return self._optimizefixups(newfixups)
413 413
414 414 @staticmethod
415 415 def _alldiffchunks(a, b, alines, blines):
416 416 """like mdiff.allblocks, but only care about differences"""
417 417 blocks = mdiff.allblocks(a, b, lines1=alines, lines2=blines)
418 418 for chunk, btype in blocks:
419 419 if btype != '!':
420 420 continue
421 421 yield chunk
422 422
423 423 def _buildlinelog(self):
424 424 """calculate the initial linelog based on self.content{,line}s.
425 425 this is similar to running a partial "annotate".
426 426 """
427 427 llog = linelog.linelog()
428 428 a, alines = '', []
429 429 for i in pycompat.xrange(len(self.contents)):
430 430 b, blines = self.contents[i], self.contentlines[i]
431 431 llrev = i * 2 + 1
432 432 chunks = self._alldiffchunks(a, b, alines, blines)
433 433 for a1, a2, b1, b2 in reversed(list(chunks)):
434 434 llog.replacelines(llrev, a1, a2, b1, b2)
435 435 a, alines = b, blines
436 436 return llog
437 437
438 438 def _checkoutlinelog(self):
439 439 """() -> [str]. check out file contents from linelog"""
440 440 contents = []
441 441 for i in pycompat.xrange(len(self.contents)):
442 442 rev = (i + 1) * 2
443 443 self.linelog.annotate(rev)
444 444 content = ''.join(map(self._getline, self.linelog.annotateresult))
445 445 contents.append(content)
446 446 return contents
447 447
448 448 def _checkoutlinelogwithedits(self):
449 449 """() -> [str]. prompt all lines for edit"""
450 450 alllines = self.linelog.getalllines()
451 451 # header
452 452 editortext = (_('HG: editing %s\nHG: "y" means the line to the right '
453 453 'exists in the changeset to the top\nHG:\n')
454 454 % self.fctxs[-1].path())
455 455 # [(idx, fctx)]. hide the dummy emptyfilecontext
456 456 visiblefctxs = [(i, f)
457 457 for i, f in enumerate(self.fctxs)
458 458 if not isinstance(f, emptyfilecontext)]
459 459 for i, (j, f) in enumerate(visiblefctxs):
460 460 editortext += (_('HG: %s/%s %s %s\n') %
461 461 ('|' * i, '-' * (len(visiblefctxs) - i + 1),
462 462 node.short(f.node()),
463 463 f.description().split('\n',1)[0]))
464 464 editortext += _('HG: %s\n') % ('|' * len(visiblefctxs))
465 465 # figure out the lifetime of a line, this is relatively inefficient,
466 466 # but probably fine
467 467 lineset = defaultdict(lambda: set()) # {(llrev, linenum): {llrev}}
468 468 for i, f in visiblefctxs:
469 469 self.linelog.annotate((i + 1) * 2)
470 470 for l in self.linelog.annotateresult:
471 471 lineset[l].add(i)
472 472 # append lines
473 473 for l in alllines:
474 474 editortext += (' %s : %s' %
475 475 (''.join([('y' if i in lineset[l] else ' ')
476 476 for i, _f in visiblefctxs]),
477 477 self._getline(l)))
478 478 # run editor
479 479 editedtext = self.ui.edit(editortext, '', action='absorb')
480 480 if not editedtext:
481 481 raise error.Abort(_('empty editor text'))
482 482 # parse edited result
483 483 contents = ['' for i in self.fctxs]
484 484 leftpadpos = 4
485 485 colonpos = leftpadpos + len(visiblefctxs) + 1
486 486 for l in mdiff.splitnewlines(editedtext):
487 487 if l.startswith('HG:'):
488 488 continue
489 489 if l[colonpos - 1:colonpos + 2] != ' : ':
490 490 raise error.Abort(_('malformed line: %s') % l)
491 491 linecontent = l[colonpos + 2:]
492 492 for i, ch in enumerate(l[leftpadpos:colonpos - 1]):
493 493 if ch == 'y':
494 494 contents[visiblefctxs[i][0]] += linecontent
495 495 # chunkstats is hard to calculate if anything changes, therefore
496 496 # set them to just a simple value (1, 1).
497 497 if editedtext != editortext:
498 498 self.chunkstats = [1, 1]
499 499 return contents
500 500
501 501 def _getline(self, lineinfo):
502 502 """((rev, linenum)) -> str. convert rev+line number to line content"""
503 503 rev, linenum = lineinfo
504 504 if rev & 1: # odd: original line taken from fctxs
505 505 return self.contentlines[rev // 2][linenum]
506 506 else: # even: fixup line from targetfctx
507 507 return self.targetlines[linenum]
508 508
509 509 def _iscontinuous(self, a1, a2, closedinterval=False):
510 510 """(a1, a2 : int) -> bool
511 511
512 512 check if these lines are continuous. i.e. no other insertions or
513 513 deletions (from other revisions) among these lines.
514 514
515 515 closedinterval decides whether a2 should be included or not. i.e. is
516 516 it [a1, a2), or [a1, a2] ?
517 517 """
518 518 if a1 >= a2:
519 519 return True
520 520 llog = self.linelog
521 521 offset1 = llog.getoffset(a1)
522 522 offset2 = llog.getoffset(a2) + int(closedinterval)
523 523 linesinbetween = llog.getalllines(offset1, offset2)
524 524 return len(linesinbetween) == a2 - a1 + int(closedinterval)
525 525
526 526 def _optimizefixups(self, fixups):
527 527 """[(rev, a1, a2, b1, b2)] -> [(rev, a1, a2, b1, b2)].
528 528 merge adjacent fixups to make them less fragmented.
529 529 """
530 530 result = []
531 531 pcurrentchunk = [[-1, -1, -1, -1, -1]]
532 532
533 533 def pushchunk():
534 534 if pcurrentchunk[0][0] != -1:
535 535 result.append(tuple(pcurrentchunk[0]))
536 536
537 537 for i, chunk in enumerate(fixups):
538 538 rev, a1, a2, b1, b2 = chunk
539 539 lastrev = pcurrentchunk[0][0]
540 540 lasta2 = pcurrentchunk[0][2]
541 541 lastb2 = pcurrentchunk[0][4]
542 542 if (a1 == lasta2 and b1 == lastb2 and rev == lastrev and
543 543 self._iscontinuous(max(a1 - 1, 0), a1)):
544 544 # merge into currentchunk
545 545 pcurrentchunk[0][2] = a2
546 546 pcurrentchunk[0][4] = b2
547 547 else:
548 548 pushchunk()
549 549 pcurrentchunk[0] = list(chunk)
550 550 pushchunk()
551 551 return result
552 552
553 553 def _showchanges(self, fm, alines, blines, chunk, fixups):
554 554
555 555 def trim(line):
556 556 if line.endswith('\n'):
557 557 line = line[:-1]
558 558 return line
559 559
560 560 # this is not optimized for perf but _showchanges only gets executed
561 561 # with an extra command-line flag.
562 562 a1, a2, b1, b2 = chunk
563 563 aidxs, bidxs = [0] * (a2 - a1), [0] * (b2 - b1)
564 564 for idx, fa1, fa2, fb1, fb2 in fixups:
565 565 for i in pycompat.xrange(fa1, fa2):
566 566 aidxs[i - a1] = (max(idx, 1) - 1) // 2
567 567 for i in pycompat.xrange(fb1, fb2):
568 568 bidxs[i - b1] = (max(idx, 1) - 1) // 2
569 569
570 570 fm.startitem()
571 571 fm.write('hunk', ' %s\n',
572 572 '@@ -%d,%d +%d,%d @@'
573 573 % (a1, a2 - a1, b1, b2 - b1), label='diff.hunk')
574 574 fm.data(path=self.path, linetype='hunk')
575 575
576 576 def writeline(idx, diffchar, line, linetype, linelabel):
577 577 fm.startitem()
578 578 node = ''
579 579 if idx:
580 580 ctx = self.fctxs[idx]
581 581 fm.context(fctx=ctx)
582 582 node = ctx.hex()
583 583 self.ctxaffected.add(ctx.changectx())
584 584 fm.write('node', '%-7.7s ', node, label='absorb.node')
585 585 fm.write('diffchar ' + linetype, '%s%s\n', diffchar, line,
586 586 label=linelabel)
587 587 fm.data(path=self.path, linetype=linetype)
588 588
589 589 for i in pycompat.xrange(a1, a2):
590 590 writeline(aidxs[i - a1], '-', trim(alines[i]), 'deleted',
591 591 'diff.deleted')
592 592 for i in pycompat.xrange(b1, b2):
593 593 writeline(bidxs[i - b1], '+', trim(blines[i]), 'inserted',
594 594 'diff.inserted')
595 595
596 596 class fixupstate(object):
597 597 """state needed to run absorb
598 598
599 599 internally, it keeps paths and filefixupstates.
600 600
601 601 a typical use is like filefixupstates:
602 602
603 603 1. call diffwith, to calculate fixups
604 604 2. (optionally), present fixups to the user, or edit fixups
605 605 3. call apply, to apply changes to memory
606 606 4. call commit, to commit changes to hg database
607 607 """
608 608
609 609 def __init__(self, stack, ui=None, opts=None):
610 610 """([ctx], ui or None) -> None
611 611
612 612 stack: should be linear, and sorted by topo order - oldest first.
613 613 all commits in stack are considered mutable.
614 614 """
615 615 assert stack
616 616 self.ui = ui or nullui()
617 617 self.opts = opts or {}
618 618 self.stack = stack
619 619 self.repo = stack[-1].repo().unfiltered()
620 620
621 621 # following fields will be filled later
622 622 self.paths = [] # [str]
623 623 self.status = None # ctx.status output
624 624 self.fctxmap = {} # {path: {ctx: fctx}}
625 625 self.fixupmap = {} # {path: filefixupstate}
626 626 self.replacemap = {} # {oldnode: newnode or None}
627 627 self.finalnode = None # head after all fixups
628 628 self.ctxaffected = set() # ctx that will be absorbed into
629 629
630 630 def diffwith(self, targetctx, match=None, fm=None):
631 631 """diff and prepare fixups. update self.fixupmap, self.paths"""
632 632 # only care about modified files
633 633 self.status = self.stack[-1].status(targetctx, match)
634 634 self.paths = []
635 635 # but if --edit-lines is used, the user may want to edit files
636 636 # even if they are not modified
637 637 editopt = self.opts.get('edit_lines')
638 638 if not self.status.modified and editopt and match:
639 639 interestingpaths = match.files()
640 640 else:
641 641 interestingpaths = self.status.modified
642 642 # prepare the filefixupstate
643 643 seenfctxs = set()
644 644 # sorting is necessary to eliminate ambiguity for the "double move"
645 645 # case: "hg cp A B; hg cp A C; hg rm A", then only "B" can affect "A".
646 646 for path in sorted(interestingpaths):
647 647 self.ui.debug('calculating fixups for %s\n' % path)
648 648 targetfctx = targetctx[path]
649 649 fctxs, ctx2fctx = getfilestack(self.stack, path, seenfctxs)
650 650 # ignore symbolic links or binary, or unchanged files
651 651 if any(f.islink() or stringutil.binary(f.data())
652 652 for f in [targetfctx] + fctxs
653 653 if not isinstance(f, emptyfilecontext)):
654 654 continue
655 655 if targetfctx.data() == fctxs[-1].data() and not editopt:
656 656 continue
657 657 seenfctxs.update(fctxs[1:])
658 658 self.fctxmap[path] = ctx2fctx
659 659 fstate = filefixupstate(fctxs, path, ui=self.ui, opts=self.opts)
660 660 if fm is not None:
661 661 fm.startitem()
662 662 fm.plain('showing changes for ')
663 663 fm.write('path', '%s\n', path, label='absorb.path')
664 664 fm.data(linetype='path')
665 665 fstate.diffwith(targetfctx, fm)
666 666 self.fixupmap[path] = fstate
667 667 self.paths.append(path)
668 668 self.ctxaffected.update(fstate.ctxaffected)
669 669
670 670 def apply(self):
671 671 """apply fixups to individual filefixupstates"""
672 672 for path, state in self.fixupmap.iteritems():
673 673 if self.ui.debugflag:
674 674 self.ui.write(_('applying fixups to %s\n') % path)
675 675 state.apply()
676 676
677 677 @property
678 678 def chunkstats(self):
679 679 """-> {path: chunkstats}. collect chunkstats from filefixupstates"""
680 680 return dict((path, state.chunkstats)
681 681 for path, state in self.fixupmap.iteritems())
682 682
683 683 def commit(self):
684 684 """commit changes. update self.finalnode, self.replacemap"""
685 685 with self.repo.wlock(), self.repo.lock():
686 686 with self.repo.transaction('absorb') as tr:
687 687 self._commitstack()
688 688 self._movebookmarks(tr)
689 689 if self.repo['.'].node() in self.replacemap:
690 690 self._moveworkingdirectoryparent()
691 691 if self._useobsolete:
692 692 self._obsoleteoldcommits()
693 693 if not self._useobsolete: # strip must be outside transactions
694 694 self._stripoldcommits()
695 695 return self.finalnode
696 696
697 697 def printchunkstats(self):
698 698 """print things like '1 of 2 chunk(s) applied'"""
699 699 ui = self.ui
700 700 chunkstats = self.chunkstats
701 701 if ui.verbose:
702 702 # chunkstats for each file
703 703 for path, stat in chunkstats.iteritems():
704 704 if stat[0]:
705 705 ui.write(_('%s: %d of %d chunk(s) applied\n')
706 706 % (path, stat[0], stat[1]))
707 707 elif not ui.quiet:
708 708 # a summary for all files
709 709 stats = chunkstats.values()
710 710 applied, total = (sum(s[i] for s in stats) for i in (0, 1))
711 711 ui.write(_('%d of %d chunk(s) applied\n') % (applied, total))
712 712
713 713 def _commitstack(self):
714 714 """make new commits. update self.finalnode, self.replacemap.
715 715 it is splitted from "commit" to avoid too much indentation.
716 716 """
717 717 # last node (20-char) committed by us
718 718 lastcommitted = None
719 719 # p1 which overrides the parent of the next commit, "None" means use
720 720 # the original parent unchanged
721 721 nextp1 = None
722 722 for ctx in self.stack:
723 723 memworkingcopy = self._getnewfilecontents(ctx)
724 724 if not memworkingcopy and not lastcommitted:
725 725 # nothing changed, nothing commited
726 726 nextp1 = ctx
727 727 continue
728 728 msg = ''
729 729 if self._willbecomenoop(memworkingcopy, ctx, nextp1):
730 730 # changeset is no longer necessary
731 731 self.replacemap[ctx.node()] = None
732 732 msg = _('became empty and was dropped')
733 733 else:
734 734 # changeset needs re-commit
735 735 nodestr = self._commitsingle(memworkingcopy, ctx, p1=nextp1)
736 736 lastcommitted = self.repo[nodestr]
737 737 nextp1 = lastcommitted
738 738 self.replacemap[ctx.node()] = lastcommitted.node()
739 739 if memworkingcopy:
740 740 msg = _('%d file(s) changed, became %s') % (
741 741 len(memworkingcopy), self._ctx2str(lastcommitted))
742 742 else:
743 743 msg = _('became %s') % self._ctx2str(lastcommitted)
744 744 if self.ui.verbose and msg:
745 745 self.ui.write(_('%s: %s\n') % (self._ctx2str(ctx), msg))
746 746 self.finalnode = lastcommitted and lastcommitted.node()
747 747
748 748 def _ctx2str(self, ctx):
749 749 if self.ui.debugflag:
750 750 return '%d:%s' % (ctx.rev(), ctx.hex())
751 751 else:
752 752 return '%d:%s' % (ctx.rev(), node.short(ctx.node()))
753 753
754 754 def _getnewfilecontents(self, ctx):
755 755 """(ctx) -> {path: str}
756 756
757 757 fetch file contents from filefixupstates.
758 758 return the working copy overrides - files different from ctx.
759 759 """
760 760 result = {}
761 761 for path in self.paths:
762 762 ctx2fctx = self.fctxmap[path] # {ctx: fctx}
763 763 if ctx not in ctx2fctx:
764 764 continue
765 765 fctx = ctx2fctx[ctx]
766 766 content = fctx.data()
767 767 newcontent = self.fixupmap[path].getfinalcontent(fctx)
768 768 if content != newcontent:
769 769 result[fctx.path()] = newcontent
770 770 return result
771 771
772 772 def _movebookmarks(self, tr):
773 773 repo = self.repo
774 774 needupdate = [(name, self.replacemap[hsh])
775 775 for name, hsh in repo._bookmarks.iteritems()
776 776 if hsh in self.replacemap]
777 777 changes = []
778 778 for name, hsh in needupdate:
779 779 if hsh:
780 780 changes.append((name, hsh))
781 781 if self.ui.verbose:
782 782 self.ui.write(_('moving bookmark %s to %s\n')
783 783 % (name, node.hex(hsh)))
784 784 else:
785 785 changes.append((name, None))
786 786 if self.ui.verbose:
787 787 self.ui.write(_('deleting bookmark %s\n') % name)
788 788 repo._bookmarks.applychanges(repo, tr, changes)
789 789
790 790 def _moveworkingdirectoryparent(self):
791 791 if not self.finalnode:
792 792 # Find the latest not-{obsoleted,stripped} parent.
793 793 revs = self.repo.revs('max(::. - %ln)', self.replacemap.keys())
794 794 ctx = self.repo[revs.first()]
795 795 self.finalnode = ctx.node()
796 796 else:
797 797 ctx = self.repo[self.finalnode]
798 798
799 799 dirstate = self.repo.dirstate
800 800 # dirstate.rebuild invalidates fsmonitorstate, causing "hg status" to
801 801 # be slow. in absorb's case, no need to invalidate fsmonitorstate.
802 802 noop = lambda: 0
803 803 restore = noop
804 804 if util.safehasattr(dirstate, '_fsmonitorstate'):
805 805 bak = dirstate._fsmonitorstate.invalidate
806 806 def restore():
807 807 dirstate._fsmonitorstate.invalidate = bak
808 808 dirstate._fsmonitorstate.invalidate = noop
809 809 try:
810 810 with dirstate.parentchange():
811 811 dirstate.rebuild(ctx.node(), ctx.manifest(), self.paths)
812 812 finally:
813 813 restore()
814 814
815 815 @staticmethod
816 816 def _willbecomenoop(memworkingcopy, ctx, pctx=None):
817 817 """({path: content}, ctx, ctx) -> bool. test if a commit will be noop
818 818
819 819 if it will become an empty commit (does not change anything, after the
820 820 memworkingcopy overrides), return True. otherwise return False.
821 821 """
822 822 if not pctx:
823 823 parents = ctx.parents()
824 824 if len(parents) != 1:
825 825 return False
826 826 pctx = parents[0]
827 827 # ctx changes more files (not a subset of memworkingcopy)
828 828 if not set(ctx.files()).issubset(set(memworkingcopy)):
829 829 return False
830 830 for path, content in memworkingcopy.iteritems():
831 831 if path not in pctx or path not in ctx:
832 832 return False
833 833 fctx = ctx[path]
834 834 pfctx = pctx[path]
835 835 if pfctx.flags() != fctx.flags():
836 836 return False
837 837 if pfctx.data() != content:
838 838 return False
839 839 return True
840 840
841 841 def _commitsingle(self, memworkingcopy, ctx, p1=None):
842 842 """(ctx, {path: content}, node) -> node. make a single commit
843 843
844 844 the commit is a clone from ctx, with a (optionally) different p1, and
845 845 different file contents replaced by memworkingcopy.
846 846 """
847 847 parents = p1 and (p1, node.nullid)
848 848 extra = ctx.extra()
849 849 if self._useobsolete and self.ui.configbool('absorb', 'add-noise'):
850 850 extra['absorb_source'] = ctx.hex()
851 851 mctx = overlaycontext(memworkingcopy, ctx, parents, extra=extra)
852 852 # preserve phase
853 853 with mctx.repo().ui.configoverride({
854 854 ('phases', 'new-commit'): ctx.phase()}):
855 855 return mctx.commit()
856 856
857 857 @util.propertycache
858 858 def _useobsolete(self):
859 859 """() -> bool"""
860 860 return obsolete.isenabled(self.repo, obsolete.createmarkersopt)
861 861
862 862 def _obsoleteoldcommits(self):
863 863 relations = [(self.repo[k], v and (self.repo[v],) or ())
864 864 for k, v in self.replacemap.iteritems()]
865 865 if relations:
866 866 obsolete.createmarkers(self.repo, relations)
867 867
868 868 def _stripoldcommits(self):
869 869 nodelist = self.replacemap.keys()
870 870 # make sure we don't strip innocent children
871 871 revs = self.repo.revs('%ln - (::(heads(%ln::)-%ln))', nodelist,
872 872 nodelist, nodelist)
873 873 tonode = self.repo.changelog.node
874 874 nodelist = [tonode(r) for r in revs]
875 875 if nodelist:
876 876 repair.strip(self.repo.ui, self.repo, nodelist)
877 877
878 878 def _parsechunk(hunk):
879 879 """(crecord.uihunk or patch.recordhunk) -> (path, (a1, a2, [bline]))"""
880 880 if type(hunk) not in (crecord.uihunk, patch.recordhunk):
881 881 return None, None
882 882 path = hunk.header.filename()
883 883 a1 = hunk.fromline + len(hunk.before) - 1
884 884 # remove before and after context
885 885 hunk.before = hunk.after = []
886 886 buf = util.stringio()
887 887 hunk.write(buf)
888 888 patchlines = mdiff.splitnewlines(buf.getvalue())
889 889 # hunk.prettystr() will update hunk.removed
890 890 a2 = a1 + hunk.removed
891 891 blines = [l[1:] for l in patchlines[1:] if l[0] != '-']
892 892 return path, (a1, a2, blines)
893 893
894 894 def overlaydiffcontext(ctx, chunks):
895 895 """(ctx, [crecord.uihunk]) -> memctx
896 896
897 897 return a memctx with some [1] patches (chunks) applied to ctx.
898 898 [1]: modifications are handled. renames, mode changes, etc. are ignored.
899 899 """
900 900 # sadly the applying-patch logic is hardly reusable, and messy:
901 901 # 1. the core logic "_applydiff" is too heavy - it writes .rej files, it
902 902 # needs a file stream of a patch and will re-parse it, while we have
903 903 # structured hunk objects at hand.
904 904 # 2. a lot of different implementations about "chunk" (patch.hunk,
905 905 # patch.recordhunk, crecord.uihunk)
906 906 # as we only care about applying changes to modified files, no mode
907 907 # change, no binary diff, and no renames, it's probably okay to
908 908 # re-invent the logic using much simpler code here.
909 909 memworkingcopy = {} # {path: content}
910 910 patchmap = defaultdict(lambda: []) # {path: [(a1, a2, [bline])]}
911 911 for path, info in map(_parsechunk, chunks):
912 912 if not path or not info:
913 913 continue
914 914 patchmap[path].append(info)
915 915 for path, patches in patchmap.iteritems():
916 916 if path not in ctx or not patches:
917 917 continue
918 918 patches.sort(reverse=True)
919 919 lines = mdiff.splitnewlines(ctx[path].data())
920 920 for a1, a2, blines in patches:
921 921 lines[a1:a2] = blines
922 922 memworkingcopy[path] = ''.join(lines)
923 923 return overlaycontext(memworkingcopy, ctx)
924 924
925 925 def absorb(ui, repo, stack=None, targetctx=None, pats=None, opts=None):
926 926 """pick fixup chunks from targetctx, apply them to stack.
927 927
928 928 if targetctx is None, the working copy context will be used.
929 929 if stack is None, the current draft stack will be used.
930 930 return fixupstate.
931 931 """
932 932 if stack is None:
933 933 limit = ui.configint('absorb', 'max-stack-size')
934 934 stack = getdraftstack(repo['.'], limit)
935 935 if limit and len(stack) >= limit:
936 936 ui.warn(_('absorb: only the recent %d changesets will '
937 937 'be analysed\n')
938 938 % limit)
939 939 if not stack:
940 940 raise error.Abort(_('no mutable changeset to change'))
941 941 if targetctx is None: # default to working copy
942 942 targetctx = repo[None]
943 943 if pats is None:
944 944 pats = ()
945 945 if opts is None:
946 946 opts = {}
947 947 state = fixupstate(stack, ui=ui, opts=opts)
948 948 matcher = scmutil.match(targetctx, pats, opts)
949 949 if opts.get('interactive'):
950 950 diff = patch.diff(repo, stack[-1].node(), targetctx.node(), matcher)
951 951 origchunks = patch.parsepatch(diff)
952 952 chunks = cmdutil.recordfilter(ui, origchunks)[0]
953 953 targetctx = overlaydiffcontext(stack[-1], chunks)
954 954 fm = None
955 955 if opts.get('print_changes') or not opts.get('apply_changes'):
956 956 fm = ui.formatter('absorb', opts)
957 957 state.diffwith(targetctx, matcher, fm)
958 958 if fm is not None:
959 959 fm.startitem()
960 960 fm.write("count", "\n%d changesets affected\n", len(state.ctxaffected))
961 961 fm.data(linetype='summary')
962 962 for ctx in reversed(stack):
963 963 if ctx not in state.ctxaffected:
964 964 continue
965 965 fm.startitem()
966 966 fm.context(ctx=ctx)
967 967 fm.data(linetype='changeset')
968 968 fm.write('node', '%-7.7s ', ctx.hex(), label='absorb.node')
969 969 descfirstline = ctx.description().splitlines()[0]
970 970 fm.write('descfirstline', '%s\n', descfirstline,
971 971 label='absorb.description')
972 972 fm.end()
973 973 if not opts.get('dry_run'):
974 974 if not opts.get('apply_changes'):
975 975 if ui.promptchoice("apply changes (yn)? $$ &Yes $$ &No", default=1):
976 976 raise error.Abort(_('absorb cancelled\n'))
977 977
978 978 state.apply()
979 979 if state.commit():
980 980 state.printchunkstats()
981 981 elif not ui.quiet:
982 982 ui.write(_('nothing applied\n'))
983 983 return state
984 984
985 985 @command('^absorb',
986 986 [('a', 'apply-changes', None,
987 987 _('apply changes without prompting for confirmation')),
988 988 ('p', 'print-changes', None,
989 989 _('always print which changesets are modified by which changes')),
990 990 ('i', 'interactive', None,
991 991 _('interactively select which chunks to apply (EXPERIMENTAL)')),
992 992 ('e', 'edit-lines', None,
993 993 _('edit what lines belong to which changesets before commit '
994 994 '(EXPERIMENTAL)')),
995 995 ] + commands.dryrunopts + commands.templateopts + commands.walkopts,
996 _('hg absorb [OPTION] [FILE]...'))
996 _('hg absorb [OPTION] [FILE]...'),
997 helpcategory=command.CATEGORY_COMMITTING)
997 998 def absorbcmd(ui, repo, *pats, **opts):
998 999 """incorporate corrections into the stack of draft changesets
999 1000
1000 1001 absorb analyzes each change in your working directory and attempts to
1001 1002 amend the changed lines into the changesets in your stack that first
1002 1003 introduced those lines.
1003 1004
1004 1005 If absorb cannot find an unambiguous changeset to amend for a change,
1005 1006 that change will be left in the working directory, untouched. They can be
1006 1007 observed by :hg:`status` or :hg:`diff` afterwards. In other words,
1007 1008 absorb does not write to the working directory.
1008 1009
1009 1010 Changesets outside the revset `::. and not public() and not merge()` will
1010 1011 not be changed.
1011 1012
1012 1013 Changesets that become empty after applying the changes will be deleted.
1013 1014
1014 1015 By default, absorb will show what it plans to do and prompt for
1015 1016 confirmation. If you are confident that the changes will be absorbed
1016 1017 to the correct place, run :hg:`absorb -a` to apply the changes
1017 1018 immediately.
1018 1019
1019 1020 Returns 0 on success, 1 if all chunks were ignored and nothing amended.
1020 1021 """
1021 1022 opts = pycompat.byteskwargs(opts)
1022 1023 state = absorb(ui, repo, pats=pats, opts=opts)
1023 1024 if sum(s[0] for s in state.chunkstats.values()) == 0:
1024 1025 return 1
@@ -1,57 +1,58 b''
1 1 # amend.py - provide the amend command
2 2 #
3 3 # Copyright 2017 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 """provide the amend command (EXPERIMENTAL)
8 8
9 9 This extension provides an ``amend`` command that is similar to
10 10 ``commit --amend`` but does not prompt an editor.
11 11 """
12 12
13 13 from __future__ import absolute_import
14 14
15 15 from mercurial.i18n import _
16 16 from mercurial import (
17 17 cmdutil,
18 18 commands,
19 19 error,
20 20 pycompat,
21 21 registrar,
22 22 )
23 23
24 24 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
25 25 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
26 26 # be specifying the version(s) of Mercurial they are tested with, or
27 27 # leave the attribute unspecified.
28 28 testedwith = 'ships-with-hg-core'
29 29
30 30 cmdtable = {}
31 31 command = registrar.command(cmdtable)
32 32
33 33 @command('amend',
34 34 [('A', 'addremove', None,
35 35 _('mark new/missing files as added/removed before committing')),
36 36 ('e', 'edit', None, _('invoke editor on commit messages')),
37 37 ('i', 'interactive', None, _('use interactive mode')),
38 38 ('n', 'note', '', _('store a note on the amend')),
39 39 ] + cmdutil.walkopts + cmdutil.commitopts + cmdutil.commitopts2,
40 40 _('[OPTION]... [FILE]...'),
41 helpcategory=command.CATEGORY_COMMITTING,
41 42 inferrepo=True)
42 43 def amend(ui, repo, *pats, **opts):
43 44 """amend the working copy parent with all or specified outstanding changes
44 45
45 46 Similar to :hg:`commit --amend`, but reuse the commit message without
46 47 invoking editor, unless ``--edit`` was set.
47 48
48 49 See :hg:`help commit` for more details.
49 50 """
50 51 opts = pycompat.byteskwargs(opts)
51 52 if len(opts['note']) > 255:
52 53 raise error.Abort(_("cannot store a note of more than 255 bytes"))
53 54 with repo.wlock(), repo.lock():
54 55 if not opts.get('logfile'):
55 56 opts['message'] = opts.get('message') or repo['.'].description()
56 57 opts['amend'] = True
57 58 return commands._docommit(ui, repo, *pats, **pycompat.strkwargs(opts))
@@ -1,255 +1,256 b''
1 1 # blackbox.py - log repository events to a file for post-mortem debugging
2 2 #
3 3 # Copyright 2010 Nicolas Dumazet
4 4 # Copyright 2013 Facebook, Inc.
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 """log repository events to a blackbox for debugging
10 10
11 11 Logs event information to .hg/blackbox.log to help debug and diagnose problems.
12 12 The events that get logged can be configured via the blackbox.track config key.
13 13
14 14 Examples::
15 15
16 16 [blackbox]
17 17 track = *
18 18 # dirty is *EXPENSIVE* (slow);
19 19 # each log entry indicates `+` if the repository is dirty, like :hg:`id`.
20 20 dirty = True
21 21 # record the source of log messages
22 22 logsource = True
23 23
24 24 [blackbox]
25 25 track = command, commandfinish, commandexception, exthook, pythonhook
26 26
27 27 [blackbox]
28 28 track = incoming
29 29
30 30 [blackbox]
31 31 # limit the size of a log file
32 32 maxsize = 1.5 MB
33 33 # rotate up to N log files when the current one gets too big
34 34 maxfiles = 3
35 35
36 36 """
37 37
38 38 from __future__ import absolute_import
39 39
40 40 import errno
41 41 import re
42 42
43 43 from mercurial.i18n import _
44 44 from mercurial.node import hex
45 45
46 46 from mercurial import (
47 47 encoding,
48 48 pycompat,
49 49 registrar,
50 50 ui as uimod,
51 51 util,
52 52 )
53 53 from mercurial.utils import (
54 54 dateutil,
55 55 procutil,
56 56 )
57 57
58 58 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
59 59 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
60 60 # be specifying the version(s) of Mercurial they are tested with, or
61 61 # leave the attribute unspecified.
62 62 testedwith = 'ships-with-hg-core'
63 63
64 64 cmdtable = {}
65 65 command = registrar.command(cmdtable)
66 66
67 67 configtable = {}
68 68 configitem = registrar.configitem(configtable)
69 69
70 70 configitem('blackbox', 'dirty',
71 71 default=False,
72 72 )
73 73 configitem('blackbox', 'maxsize',
74 74 default='1 MB',
75 75 )
76 76 configitem('blackbox', 'logsource',
77 77 default=False,
78 78 )
79 79 configitem('blackbox', 'maxfiles',
80 80 default=7,
81 81 )
82 82 configitem('blackbox', 'track',
83 83 default=lambda: ['*'],
84 84 )
85 85
86 86 lastui = None
87 87
88 88 def _openlogfile(ui, vfs):
89 89 def rotate(oldpath, newpath):
90 90 try:
91 91 vfs.unlink(newpath)
92 92 except OSError as err:
93 93 if err.errno != errno.ENOENT:
94 94 ui.debug("warning: cannot remove '%s': %s\n" %
95 95 (newpath, err.strerror))
96 96 try:
97 97 if newpath:
98 98 vfs.rename(oldpath, newpath)
99 99 except OSError as err:
100 100 if err.errno != errno.ENOENT:
101 101 ui.debug("warning: cannot rename '%s' to '%s': %s\n" %
102 102 (newpath, oldpath, err.strerror))
103 103
104 104 maxsize = ui.configbytes('blackbox', 'maxsize')
105 105 name = 'blackbox.log'
106 106 if maxsize > 0:
107 107 try:
108 108 st = vfs.stat(name)
109 109 except OSError:
110 110 pass
111 111 else:
112 112 if st.st_size >= maxsize:
113 113 path = vfs.join(name)
114 114 maxfiles = ui.configint('blackbox', 'maxfiles')
115 115 for i in pycompat.xrange(maxfiles - 1, 1, -1):
116 116 rotate(oldpath='%s.%d' % (path, i - 1),
117 117 newpath='%s.%d' % (path, i))
118 118 rotate(oldpath=path,
119 119 newpath=maxfiles > 0 and path + '.1')
120 120 return vfs(name, 'a')
121 121
122 122 def wrapui(ui):
123 123 class blackboxui(ui.__class__):
124 124 @property
125 125 def _bbvfs(self):
126 126 vfs = None
127 127 repo = getattr(self, '_bbrepo', None)
128 128 if repo:
129 129 vfs = repo.vfs
130 130 if not vfs.isdir('.'):
131 131 vfs = None
132 132 return vfs
133 133
134 134 @util.propertycache
135 135 def track(self):
136 136 return self.configlist('blackbox', 'track')
137 137
138 138 def debug(self, *msg, **opts):
139 139 super(blackboxui, self).debug(*msg, **opts)
140 140 if self.debugflag:
141 141 self.log('debug', '%s', ''.join(msg))
142 142
143 143 def log(self, event, *msg, **opts):
144 144 global lastui
145 145 super(blackboxui, self).log(event, *msg, **opts)
146 146
147 147 if not '*' in self.track and not event in self.track:
148 148 return
149 149
150 150 if self._bbvfs:
151 151 ui = self
152 152 else:
153 153 # certain ui instances exist outside the context of
154 154 # a repo, so just default to the last blackbox that
155 155 # was seen.
156 156 ui = lastui
157 157
158 158 if not ui:
159 159 return
160 160 vfs = ui._bbvfs
161 161 if not vfs:
162 162 return
163 163
164 164 repo = getattr(ui, '_bbrepo', None)
165 165 if not lastui or repo:
166 166 lastui = ui
167 167 if getattr(ui, '_bbinlog', False):
168 168 # recursion and failure guard
169 169 return
170 170 ui._bbinlog = True
171 171 default = self.configdate('devel', 'default-date')
172 172 date = dateutil.datestr(default, '%Y/%m/%d %H:%M:%S')
173 173 user = procutil.getuser()
174 174 pid = '%d' % procutil.getpid()
175 175 formattedmsg = msg[0] % msg[1:]
176 176 rev = '(unknown)'
177 177 changed = ''
178 178 if repo:
179 179 ctx = repo[None]
180 180 parents = ctx.parents()
181 181 rev = ('+'.join([hex(p.node()) for p in parents]))
182 182 if (ui.configbool('blackbox', 'dirty') and
183 183 ctx.dirty(missing=True, merge=False, branch=False)):
184 184 changed = '+'
185 185 if ui.configbool('blackbox', 'logsource'):
186 186 src = ' [%s]' % event
187 187 else:
188 188 src = ''
189 189 try:
190 190 fmt = '%s %s @%s%s (%s)%s> %s'
191 191 args = (date, user, rev, changed, pid, src, formattedmsg)
192 192 with _openlogfile(ui, vfs) as fp:
193 193 fp.write(fmt % args)
194 194 except (IOError, OSError) as err:
195 195 self.debug('warning: cannot write to blackbox.log: %s\n' %
196 196 encoding.strtolocal(err.strerror))
197 197 # do not restore _bbinlog intentionally to avoid failed
198 198 # logging again
199 199 else:
200 200 ui._bbinlog = False
201 201
202 202 def setrepo(self, repo):
203 203 self._bbrepo = repo
204 204
205 205 ui.__class__ = blackboxui
206 206 uimod.ui = blackboxui
207 207
208 208 def uisetup(ui):
209 209 wrapui(ui)
210 210
211 211 def reposetup(ui, repo):
212 212 # During 'hg pull' a httppeer repo is created to represent the remote repo.
213 213 # It doesn't have a .hg directory to put a blackbox in, so we don't do
214 214 # the blackbox setup for it.
215 215 if not repo.local():
216 216 return
217 217
218 218 if util.safehasattr(ui, 'setrepo'):
219 219 ui.setrepo(repo)
220 220
221 221 # Set lastui even if ui.log is not called. This gives blackbox a
222 222 # fallback place to log.
223 223 global lastui
224 224 if lastui is None:
225 225 lastui = ui
226 226
227 227 repo._wlockfreeprefix.add('blackbox.log')
228 228
229 229 @command('^blackbox',
230 230 [('l', 'limit', 10, _('the number of events to show')),
231 231 ],
232 _('hg blackbox [OPTION]...'))
232 _('hg blackbox [OPTION]...'),
233 helpcategory=command.CATEGORY_MAINTENANCE)
233 234 def blackbox(ui, repo, *revs, **opts):
234 235 '''view the recent repository events
235 236 '''
236 237
237 238 if not repo.vfs.exists('blackbox.log'):
238 239 return
239 240
240 241 limit = opts.get(r'limit')
241 242 fp = repo.vfs('blackbox.log', 'r')
242 243 lines = fp.read().split('\n')
243 244
244 245 count = 0
245 246 output = []
246 247 for line in reversed(lines):
247 248 if count >= limit:
248 249 break
249 250
250 251 # count the commands by matching lines like: 2013/01/23 19:13:36 root>
251 252 if re.match('^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} .*> .*', line):
252 253 count += 1
253 254 output.append(line)
254 255
255 256 ui.status('\n'.join(reversed(output)))
@@ -1,99 +1,100 b''
1 1 # Copyright (C) 2015 - Mike Edgar <adgar@google.com>
2 2 #
3 3 # This extension enables removal of file content at a given revision,
4 4 # rewriting the data/metadata of successive revisions to preserve revision log
5 5 # integrity.
6 6
7 7 """erase file content at a given revision
8 8
9 9 The censor command instructs Mercurial to erase all content of a file at a given
10 10 revision *without updating the changeset hash.* This allows existing history to
11 11 remain valid while preventing future clones/pulls from receiving the erased
12 12 data.
13 13
14 14 Typical uses for censor are due to security or legal requirements, including::
15 15
16 16 * Passwords, private keys, cryptographic material
17 17 * Licensed data/code/libraries for which the license has expired
18 18 * Personally Identifiable Information or other private data
19 19
20 20 Censored nodes can interrupt mercurial's typical operation whenever the excised
21 21 data needs to be materialized. Some commands, like ``hg cat``/``hg revert``,
22 22 simply fail when asked to produce censored data. Others, like ``hg verify`` and
23 23 ``hg update``, must be capable of tolerating censored data to continue to
24 24 function in a meaningful way. Such commands only tolerate censored file
25 25 revisions if they are allowed by the "censor.policy=ignore" config option.
26 26 """
27 27
28 28 from __future__ import absolute_import
29 29
30 30 from mercurial.i18n import _
31 31 from mercurial.node import short
32 32
33 33 from mercurial import (
34 34 error,
35 35 registrar,
36 36 scmutil,
37 37 )
38 38
39 39 cmdtable = {}
40 40 command = registrar.command(cmdtable)
41 41 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
42 42 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
43 43 # be specifying the version(s) of Mercurial they are tested with, or
44 44 # leave the attribute unspecified.
45 45 testedwith = 'ships-with-hg-core'
46 46
47 47 @command('censor',
48 48 [('r', 'rev', '', _('censor file from specified revision'), _('REV')),
49 49 ('t', 'tombstone', '', _('replacement tombstone data'), _('TEXT'))],
50 _('-r REV [-t TEXT] [FILE]'))
50 _('-r REV [-t TEXT] [FILE]'),
51 helpcategory=command.CATEGORY_MAINTENANCE)
51 52 def censor(ui, repo, path, rev='', tombstone='', **opts):
52 53 with repo.wlock(), repo.lock():
53 54 return _docensor(ui, repo, path, rev, tombstone, **opts)
54 55
55 56 def _docensor(ui, repo, path, rev='', tombstone='', **opts):
56 57 if not path:
57 58 raise error.Abort(_('must specify file path to censor'))
58 59 if not rev:
59 60 raise error.Abort(_('must specify revision to censor'))
60 61
61 62 wctx = repo[None]
62 63
63 64 m = scmutil.match(wctx, (path,))
64 65 if m.anypats() or len(m.files()) != 1:
65 66 raise error.Abort(_('can only specify an explicit filename'))
66 67 path = m.files()[0]
67 68 flog = repo.file(path)
68 69 if not len(flog):
69 70 raise error.Abort(_('cannot censor file with no history'))
70 71
71 72 rev = scmutil.revsingle(repo, rev, rev).rev()
72 73 try:
73 74 ctx = repo[rev]
74 75 except KeyError:
75 76 raise error.Abort(_('invalid revision identifier %s') % rev)
76 77
77 78 try:
78 79 fctx = ctx.filectx(path)
79 80 except error.LookupError:
80 81 raise error.Abort(_('file does not exist at revision %s') % rev)
81 82
82 83 fnode = fctx.filenode()
83 84 heads = []
84 85 for headnode in repo.heads():
85 86 hc = repo[headnode]
86 87 if path in hc and hc.filenode(path) == fnode:
87 88 heads.append(hc)
88 89 if heads:
89 90 headlist = ', '.join([short(c.node()) for c in heads])
90 91 raise error.Abort(_('cannot censor file in heads (%s)') % headlist,
91 92 hint=_('clean/delete and commit first'))
92 93
93 94 wp = wctx.parents()
94 95 if ctx.node() in [p.node() for p in wp]:
95 96 raise error.Abort(_('cannot censor working directory'),
96 97 hint=_('clean/delete/update first'))
97 98
98 99 with repo.transaction(b'censor') as tr:
99 100 flog.censorrevision(tr, fnode, tombstone=tombstone)
@@ -1,73 +1,74 b''
1 1 # Mercurial extension to provide the 'hg children' command
2 2 #
3 3 # Copyright 2007 by Intevation GmbH <intevation@intevation.de>
4 4 #
5 5 # Author(s):
6 6 # Thomas Arendsen Hein <thomas@intevation.de>
7 7 #
8 8 # This software may be used and distributed according to the terms of the
9 9 # GNU General Public License version 2 or any later version.
10 10
11 11 '''command to display child changesets (DEPRECATED)
12 12
13 13 This extension is deprecated. You should use :hg:`log -r
14 14 "children(REV)"` instead.
15 15 '''
16 16
17 17 from __future__ import absolute_import
18 18
19 19 from mercurial.i18n import _
20 20 from mercurial import (
21 21 cmdutil,
22 22 logcmdutil,
23 23 pycompat,
24 24 registrar,
25 25 scmutil,
26 26 )
27 27
28 28 templateopts = cmdutil.templateopts
29 29
30 30 cmdtable = {}
31 31 command = registrar.command(cmdtable)
32 32 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
33 33 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
34 34 # be specifying the version(s) of Mercurial they are tested with, or
35 35 # leave the attribute unspecified.
36 36 testedwith = 'ships-with-hg-core'
37 37
38 38 @command('children',
39 39 [('r', 'rev', '.',
40 40 _('show children of the specified revision'), _('REV')),
41 41 ] + templateopts,
42 42 _('hg children [-r REV] [FILE]'),
43 helpcategory=command.CATEGORY_CHANGE_NAVIGATION,
43 44 inferrepo=True)
44 45 def children(ui, repo, file_=None, **opts):
45 46 """show the children of the given or working directory revision
46 47
47 48 Print the children of the working directory's revisions. If a
48 49 revision is given via -r/--rev, the children of that revision will
49 50 be printed. If a file argument is given, revision in which the
50 51 file was last changed (after the working directory revision or the
51 52 argument to --rev if given) is printed.
52 53
53 54 Please use :hg:`log` instead::
54 55
55 56 hg children => hg log -r "children(.)"
56 57 hg children -r REV => hg log -r "children(REV)"
57 58
58 59 See :hg:`help log` and :hg:`help revsets.children`.
59 60
60 61 """
61 62 opts = pycompat.byteskwargs(opts)
62 63 rev = opts.get('rev')
63 64 ctx = scmutil.revsingle(repo, rev)
64 65 if file_:
65 66 fctx = repo.filectx(file_, changeid=ctx.rev())
66 67 childctxs = [fcctx.changectx() for fcctx in fctx.children()]
67 68 else:
68 69 childctxs = ctx.children()
69 70
70 71 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
71 72 for cctx in childctxs:
72 73 displayer.show(cctx)
73 74 displayer.close()
@@ -1,211 +1,212 b''
1 1 # churn.py - create a graph of revisions count grouped by template
2 2 #
3 3 # Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net>
4 4 # Copyright 2008 Alexander Solovyov <piranha@piranha.org.ua>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''command to display statistics about repository history'''
10 10
11 11 from __future__ import absolute_import, division
12 12
13 13 import datetime
14 14 import os
15 15 import time
16 16
17 17 from mercurial.i18n import _
18 18 from mercurial import (
19 19 cmdutil,
20 20 encoding,
21 21 logcmdutil,
22 22 patch,
23 23 pycompat,
24 24 registrar,
25 25 scmutil,
26 26 )
27 27 from mercurial.utils import dateutil
28 28
29 29 cmdtable = {}
30 30 command = registrar.command(cmdtable)
31 31 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
32 32 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
33 33 # be specifying the version(s) of Mercurial they are tested with, or
34 34 # leave the attribute unspecified.
35 35 testedwith = 'ships-with-hg-core'
36 36
37 37 def changedlines(ui, repo, ctx1, ctx2, fns):
38 38 added, removed = 0, 0
39 39 fmatch = scmutil.matchfiles(repo, fns)
40 40 diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
41 41 for l in diff.split('\n'):
42 42 if l.startswith("+") and not l.startswith("+++ "):
43 43 added += 1
44 44 elif l.startswith("-") and not l.startswith("--- "):
45 45 removed += 1
46 46 return (added, removed)
47 47
48 48 def countrate(ui, repo, amap, *pats, **opts):
49 49 """Calculate stats"""
50 50 opts = pycompat.byteskwargs(opts)
51 51 if opts.get('dateformat'):
52 52 def getkey(ctx):
53 53 t, tz = ctx.date()
54 54 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
55 55 return encoding.strtolocal(
56 56 date.strftime(encoding.strfromlocal(opts['dateformat'])))
57 57 else:
58 58 tmpl = opts.get('oldtemplate') or opts.get('template')
59 59 tmpl = logcmdutil.maketemplater(ui, repo, tmpl)
60 60 def getkey(ctx):
61 61 ui.pushbuffer()
62 62 tmpl.show(ctx)
63 63 return ui.popbuffer()
64 64
65 65 progress = ui.makeprogress(_('analyzing'), unit=_('revisions'),
66 66 total=len(repo))
67 67 rate = {}
68 68 df = False
69 69 if opts.get('date'):
70 70 df = dateutil.matchdate(opts['date'])
71 71
72 72 m = scmutil.match(repo[None], pats, opts)
73 73 def prep(ctx, fns):
74 74 rev = ctx.rev()
75 75 if df and not df(ctx.date()[0]): # doesn't match date format
76 76 return
77 77
78 78 key = getkey(ctx).strip()
79 79 key = amap.get(key, key) # alias remap
80 80 if opts.get('changesets'):
81 81 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
82 82 else:
83 83 parents = ctx.parents()
84 84 if len(parents) > 1:
85 85 ui.note(_('revision %d is a merge, ignoring...\n') % (rev,))
86 86 return
87 87
88 88 ctx1 = parents[0]
89 89 lines = changedlines(ui, repo, ctx1, ctx, fns)
90 90 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
91 91
92 92 progress.increment()
93 93
94 94 for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
95 95 continue
96 96
97 97 progress.complete()
98 98
99 99 return rate
100 100
101 101
102 102 @command('churn',
103 103 [('r', 'rev', [],
104 104 _('count rate for the specified revision or revset'), _('REV')),
105 105 ('d', 'date', '',
106 106 _('count rate for revisions matching date spec'), _('DATE')),
107 107 ('t', 'oldtemplate', '',
108 108 _('template to group changesets (DEPRECATED)'), _('TEMPLATE')),
109 109 ('T', 'template', '{author|email}',
110 110 _('template to group changesets'), _('TEMPLATE')),
111 111 ('f', 'dateformat', '',
112 112 _('strftime-compatible format for grouping by date'), _('FORMAT')),
113 113 ('c', 'changesets', False, _('count rate by number of changesets')),
114 114 ('s', 'sort', False, _('sort by key (default: sort by count)')),
115 115 ('', 'diffstat', False, _('display added/removed lines separately')),
116 116 ('', 'aliases', '', _('file with email aliases'), _('FILE')),
117 117 ] + cmdutil.walkopts,
118 118 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"),
119 helpcategory=command.CATEGORY_MAINTENANCE,
119 120 inferrepo=True)
120 121 def churn(ui, repo, *pats, **opts):
121 122 '''histogram of changes to the repository
122 123
123 124 This command will display a histogram representing the number
124 125 of changed lines or revisions, grouped according to the given
125 126 template. The default template will group changes by author.
126 127 The --dateformat option may be used to group the results by
127 128 date instead.
128 129
129 130 Statistics are based on the number of changed lines, or
130 131 alternatively the number of matching revisions if the
131 132 --changesets option is specified.
132 133
133 134 Examples::
134 135
135 136 # display count of changed lines for every committer
136 137 hg churn -T "{author|email}"
137 138
138 139 # display daily activity graph
139 140 hg churn -f "%H" -s -c
140 141
141 142 # display activity of developers by month
142 143 hg churn -f "%Y-%m" -s -c
143 144
144 145 # display count of lines changed in every year
145 146 hg churn -f "%Y" -s
146 147
147 148 It is possible to map alternate email addresses to a main address
148 149 by providing a file using the following format::
149 150
150 151 <alias email> = <actual email>
151 152
152 153 Such a file may be specified with the --aliases option, otherwise
153 154 a .hgchurn file will be looked for in the working directory root.
154 155 Aliases will be split from the rightmost "=".
155 156 '''
156 157 def pad(s, l):
157 158 return s + " " * (l - encoding.colwidth(s))
158 159
159 160 amap = {}
160 161 aliases = opts.get(r'aliases')
161 162 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
162 163 aliases = repo.wjoin('.hgchurn')
163 164 if aliases:
164 165 for l in open(aliases, "rb"):
165 166 try:
166 167 alias, actual = l.rsplit('=' in l and '=' or None, 1)
167 168 amap[alias.strip()] = actual.strip()
168 169 except ValueError:
169 170 l = l.strip()
170 171 if l:
171 172 ui.warn(_("skipping malformed alias: %s\n") % l)
172 173 continue
173 174
174 175 rate = list(countrate(ui, repo, amap, *pats, **opts).items())
175 176 if not rate:
176 177 return
177 178
178 179 if opts.get(r'sort'):
179 180 rate.sort()
180 181 else:
181 182 rate.sort(key=lambda x: (-sum(x[1]), x))
182 183
183 184 # Be careful not to have a zero maxcount (issue833)
184 185 maxcount = float(max(sum(v) for k, v in rate)) or 1.0
185 186 maxname = max(len(k) for k, v in rate)
186 187
187 188 ttywidth = ui.termwidth()
188 189 ui.debug("assuming %i character terminal\n" % ttywidth)
189 190 width = ttywidth - maxname - 2 - 2 - 2
190 191
191 192 if opts.get(r'diffstat'):
192 193 width -= 15
193 194 def format(name, diffstat):
194 195 added, removed = diffstat
195 196 return "%s %15s %s%s\n" % (pad(name, maxname),
196 197 '+%d/-%d' % (added, removed),
197 198 ui.label('+' * charnum(added),
198 199 'diffstat.inserted'),
199 200 ui.label('-' * charnum(removed),
200 201 'diffstat.deleted'))
201 202 else:
202 203 width -= 6
203 204 def format(name, count):
204 205 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
205 206 '*' * charnum(sum(count)))
206 207
207 208 def charnum(count):
208 209 return int(count * width // maxcount)
209 210
210 211 for name, count in rate:
211 212 ui.write(format(name, count))
@@ -1,82 +1,84 b''
1 1 # closehead.py - Close arbitrary heads without checking them out first
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 '''close arbitrary heads without checking them out first'''
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from mercurial.i18n import _
11 11 from mercurial import (
12 12 bookmarks,
13 13 cmdutil,
14 14 context,
15 15 error,
16 16 pycompat,
17 17 registrar,
18 18 scmutil,
19 19 )
20 20
21 21 cmdtable = {}
22 22 command = registrar.command(cmdtable)
23 23 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
24 24 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
25 25 # be specifying the version(s) of Mercurial they are tested with, or
26 26 # leave the attribute unspecified.
27 27 testedwith = 'ships-with-hg-core'
28 28
29 29 commitopts = cmdutil.commitopts
30 30 commitopts2 = cmdutil.commitopts2
31 31 commitopts3 = [('r', 'rev', [],
32 32 _('revision to check'), _('REV'))]
33 33
34 34 @command('close-head|close-heads', commitopts + commitopts2 + commitopts3,
35 _('[OPTION]... [REV]...'), inferrepo=True)
35 _('[OPTION]... [REV]...'),
36 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
37 inferrepo=True)
36 38 def close_branch(ui, repo, *revs, **opts):
37 39 """close the given head revisions
38 40
39 41 This is equivalent to checking out each revision in a clean tree and running
40 42 ``hg commit --close-branch``, except that it doesn't change the working
41 43 directory.
42 44
43 45 The commit message must be specified with -l or -m.
44 46 """
45 47 def docommit(rev):
46 48 cctx = context.memctx(repo, parents=[rev, None], text=message,
47 49 files=[], filectxfn=None, user=opts.get('user'),
48 50 date=opts.get('date'), extra=extra)
49 51 tr = repo.transaction('commit')
50 52 ret = repo.commitctx(cctx, True)
51 53 bookmarks.update(repo, [rev, None], ret)
52 54 cctx.markcommitted(ret)
53 55 tr.close()
54 56
55 57 opts = pycompat.byteskwargs(opts)
56 58
57 59 revs += tuple(opts.get('rev', []))
58 60 revs = scmutil.revrange(repo, revs)
59 61
60 62 if not revs:
61 63 raise error.Abort(_('no revisions specified'))
62 64
63 65 heads = []
64 66 for branch in repo.branchmap():
65 67 heads.extend(repo.branchheads(branch))
66 68 heads = set(repo[h].rev() for h in heads)
67 69 for rev in revs:
68 70 if rev not in heads:
69 71 raise error.Abort(_('revision is not an open head: %d') % rev)
70 72
71 73 message = cmdutil.logmessage(ui, opts)
72 74 if not message:
73 75 raise error.Abort(_("no commit message specified with -l or -m"))
74 76 extra = { 'close': '1' }
75 77
76 78 with repo.wlock(), repo.lock():
77 79 for rev in revs:
78 80 r = repo[rev]
79 81 branch = r.branch()
80 82 extra['branch'] = branch
81 83 docommit(r)
82 84 return 0
@@ -1,434 +1,435 b''
1 1 # extdiff.py - external diff program support for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
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 '''command to allow external programs to compare revisions
9 9
10 10 The extdiff Mercurial extension allows you to use external programs
11 11 to compare revisions, or revision with working directory. The external
12 12 diff programs are called with a configurable set of options and two
13 13 non-option arguments: paths to directories containing snapshots of
14 14 files to compare.
15 15
16 16 If there is more than one file being compared and the "child" revision
17 17 is the working directory, any modifications made in the external diff
18 18 program will be copied back to the working directory from the temporary
19 19 directory.
20 20
21 21 The extdiff extension also allows you to configure new diff commands, so
22 22 you do not need to type :hg:`extdiff -p kdiff3` always. ::
23 23
24 24 [extdiff]
25 25 # add new command that runs GNU diff(1) in 'context diff' mode
26 26 cdiff = gdiff -Nprc5
27 27 ## or the old way:
28 28 #cmd.cdiff = gdiff
29 29 #opts.cdiff = -Nprc5
30 30
31 31 # add new command called meld, runs meld (no need to name twice). If
32 32 # the meld executable is not available, the meld tool in [merge-tools]
33 33 # will be used, if available
34 34 meld =
35 35
36 36 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
37 37 # (see http://www.vim.org/scripts/script.php?script_id=102) Non
38 38 # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
39 39 # your .vimrc
40 40 vimdiff = gvim -f "+next" \\
41 41 "+execute 'DirDiff' fnameescape(argv(0)) fnameescape(argv(1))"
42 42
43 43 Tool arguments can include variables that are expanded at runtime::
44 44
45 45 $parent1, $plabel1 - filename, descriptive label of first parent
46 46 $child, $clabel - filename, descriptive label of child revision
47 47 $parent2, $plabel2 - filename, descriptive label of second parent
48 48 $root - repository root
49 49 $parent is an alias for $parent1.
50 50
51 51 The extdiff extension will look in your [diff-tools] and [merge-tools]
52 52 sections for diff tool arguments, when none are specified in [extdiff].
53 53
54 54 ::
55 55
56 56 [extdiff]
57 57 kdiff3 =
58 58
59 59 [diff-tools]
60 60 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
61 61
62 62 You can use -I/-X and list of file or directory names like normal
63 63 :hg:`diff` command. The extdiff extension makes snapshots of only
64 64 needed files, so running the external diff program will actually be
65 65 pretty fast (at least faster than having to compare the entire tree).
66 66 '''
67 67
68 68 from __future__ import absolute_import
69 69
70 70 import os
71 71 import re
72 72 import shutil
73 73 import stat
74 74
75 75 from mercurial.i18n import _
76 76 from mercurial.node import (
77 77 nullid,
78 78 short,
79 79 )
80 80 from mercurial import (
81 81 archival,
82 82 cmdutil,
83 83 error,
84 84 filemerge,
85 85 formatter,
86 86 pycompat,
87 87 registrar,
88 88 scmutil,
89 89 util,
90 90 )
91 91 from mercurial.utils import (
92 92 procutil,
93 93 stringutil,
94 94 )
95 95
96 96 cmdtable = {}
97 97 command = registrar.command(cmdtable)
98 98
99 99 configtable = {}
100 100 configitem = registrar.configitem(configtable)
101 101
102 102 configitem('extdiff', br'opts\..*',
103 103 default='',
104 104 generic=True,
105 105 )
106 106
107 107 configitem('diff-tools', br'.*\.diffargs$',
108 108 default=None,
109 109 generic=True,
110 110 )
111 111
112 112 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
113 113 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
114 114 # be specifying the version(s) of Mercurial they are tested with, or
115 115 # leave the attribute unspecified.
116 116 testedwith = 'ships-with-hg-core'
117 117
118 118 def snapshot(ui, repo, files, node, tmproot, listsubrepos):
119 119 '''snapshot files as of some revision
120 120 if not using snapshot, -I/-X does not work and recursive diff
121 121 in tools like kdiff3 and meld displays too many files.'''
122 122 dirname = os.path.basename(repo.root)
123 123 if dirname == "":
124 124 dirname = "root"
125 125 if node is not None:
126 126 dirname = '%s.%s' % (dirname, short(node))
127 127 base = os.path.join(tmproot, dirname)
128 128 os.mkdir(base)
129 129 fnsandstat = []
130 130
131 131 if node is not None:
132 132 ui.note(_('making snapshot of %d files from rev %s\n') %
133 133 (len(files), short(node)))
134 134 else:
135 135 ui.note(_('making snapshot of %d files from working directory\n') %
136 136 (len(files)))
137 137
138 138 if files:
139 139 repo.ui.setconfig("ui", "archivemeta", False)
140 140
141 141 archival.archive(repo, base, node, 'files',
142 142 matchfn=scmutil.matchfiles(repo, files),
143 143 subrepos=listsubrepos)
144 144
145 145 for fn in sorted(files):
146 146 wfn = util.pconvert(fn)
147 147 ui.note(' %s\n' % wfn)
148 148
149 149 if node is None:
150 150 dest = os.path.join(base, wfn)
151 151
152 152 fnsandstat.append((dest, repo.wjoin(fn), os.lstat(dest)))
153 153 return dirname, fnsandstat
154 154
155 155 def dodiff(ui, repo, cmdline, pats, opts):
156 156 '''Do the actual diff:
157 157
158 158 - copy to a temp structure if diffing 2 internal revisions
159 159 - copy to a temp structure if diffing working revision with
160 160 another one and more than 1 file is changed
161 161 - just invoke the diff for a single file in the working dir
162 162 '''
163 163
164 164 revs = opts.get('rev')
165 165 change = opts.get('change')
166 166 do3way = '$parent2' in cmdline
167 167
168 168 if revs and change:
169 169 msg = _('cannot specify --rev and --change at the same time')
170 170 raise error.Abort(msg)
171 171 elif change:
172 172 ctx2 = scmutil.revsingle(repo, change, None)
173 173 ctx1a, ctx1b = ctx2.p1(), ctx2.p2()
174 174 else:
175 175 ctx1a, ctx2 = scmutil.revpair(repo, revs)
176 176 if not revs:
177 177 ctx1b = repo[None].p2()
178 178 else:
179 179 ctx1b = repo[nullid]
180 180
181 181 node1a = ctx1a.node()
182 182 node1b = ctx1b.node()
183 183 node2 = ctx2.node()
184 184
185 185 # Disable 3-way merge if there is only one parent
186 186 if do3way:
187 187 if node1b == nullid:
188 188 do3way = False
189 189
190 190 subrepos=opts.get('subrepos')
191 191
192 192 matcher = scmutil.match(repo[node2], pats, opts)
193 193
194 194 if opts.get('patch'):
195 195 if subrepos:
196 196 raise error.Abort(_('--patch cannot be used with --subrepos'))
197 197 if node2 is None:
198 198 raise error.Abort(_('--patch requires two revisions'))
199 199 else:
200 200 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher,
201 201 listsubrepos=subrepos)[:3])
202 202 if do3way:
203 203 mod_b, add_b, rem_b = map(set,
204 204 repo.status(node1b, node2, matcher,
205 205 listsubrepos=subrepos)[:3])
206 206 else:
207 207 mod_b, add_b, rem_b = set(), set(), set()
208 208 modadd = mod_a | add_a | mod_b | add_b
209 209 common = modadd | rem_a | rem_b
210 210 if not common:
211 211 return 0
212 212
213 213 tmproot = pycompat.mkdtemp(prefix='extdiff.')
214 214 try:
215 215 if not opts.get('patch'):
216 216 # Always make a copy of node1a (and node1b, if applicable)
217 217 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
218 218 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot,
219 219 subrepos)[0]
220 220 rev1a = '@%d' % repo[node1a].rev()
221 221 if do3way:
222 222 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
223 223 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot,
224 224 subrepos)[0]
225 225 rev1b = '@%d' % repo[node1b].rev()
226 226 else:
227 227 dir1b = None
228 228 rev1b = ''
229 229
230 230 fnsandstat = []
231 231
232 232 # If node2 in not the wc or there is >1 change, copy it
233 233 dir2root = ''
234 234 rev2 = ''
235 235 if node2:
236 236 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
237 237 rev2 = '@%d' % repo[node2].rev()
238 238 elif len(common) > 1:
239 239 #we only actually need to get the files to copy back to
240 240 #the working dir in this case (because the other cases
241 241 #are: diffing 2 revisions or single file -- in which case
242 242 #the file is already directly passed to the diff tool).
243 243 dir2, fnsandstat = snapshot(ui, repo, modadd, None, tmproot,
244 244 subrepos)
245 245 else:
246 246 # This lets the diff tool open the changed file directly
247 247 dir2 = ''
248 248 dir2root = repo.root
249 249
250 250 label1a = rev1a
251 251 label1b = rev1b
252 252 label2 = rev2
253 253
254 254 # If only one change, diff the files instead of the directories
255 255 # Handle bogus modifies correctly by checking if the files exist
256 256 if len(common) == 1:
257 257 common_file = util.localpath(common.pop())
258 258 dir1a = os.path.join(tmproot, dir1a, common_file)
259 259 label1a = common_file + rev1a
260 260 if not os.path.isfile(dir1a):
261 261 dir1a = os.devnull
262 262 if do3way:
263 263 dir1b = os.path.join(tmproot, dir1b, common_file)
264 264 label1b = common_file + rev1b
265 265 if not os.path.isfile(dir1b):
266 266 dir1b = os.devnull
267 267 dir2 = os.path.join(dir2root, dir2, common_file)
268 268 label2 = common_file + rev2
269 269 else:
270 270 template = 'hg-%h.patch'
271 271 with formatter.nullformatter(ui, 'extdiff', {}) as fm:
272 272 cmdutil.export(repo, [repo[node1a].rev(), repo[node2].rev()],
273 273 fm,
274 274 fntemplate=repo.vfs.reljoin(tmproot, template),
275 275 match=matcher)
276 276 label1a = cmdutil.makefilename(repo[node1a], template)
277 277 label2 = cmdutil.makefilename(repo[node2], template)
278 278 dir1a = repo.vfs.reljoin(tmproot, label1a)
279 279 dir2 = repo.vfs.reljoin(tmproot, label2)
280 280 dir1b = None
281 281 label1b = None
282 282 fnsandstat = []
283 283
284 284 # Function to quote file/dir names in the argument string.
285 285 # When not operating in 3-way mode, an empty string is
286 286 # returned for parent2
287 287 replace = {'parent': dir1a, 'parent1': dir1a, 'parent2': dir1b,
288 288 'plabel1': label1a, 'plabel2': label1b,
289 289 'clabel': label2, 'child': dir2,
290 290 'root': repo.root}
291 291 def quote(match):
292 292 pre = match.group(2)
293 293 key = match.group(3)
294 294 if not do3way and key == 'parent2':
295 295 return pre
296 296 return pre + procutil.shellquote(replace[key])
297 297
298 298 # Match parent2 first, so 'parent1?' will match both parent1 and parent
299 299 regex = (br'''(['"]?)([^\s'"$]*)'''
300 300 br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1')
301 301 if not do3way and not re.search(regex, cmdline):
302 302 cmdline += ' $parent1 $child'
303 303 cmdline = re.sub(regex, quote, cmdline)
304 304
305 305 ui.debug('running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot))
306 306 ui.system(cmdline, cwd=tmproot, blockedtag='extdiff')
307 307
308 308 for copy_fn, working_fn, st in fnsandstat:
309 309 cpstat = os.lstat(copy_fn)
310 310 # Some tools copy the file and attributes, so mtime may not detect
311 311 # all changes. A size check will detect more cases, but not all.
312 312 # The only certain way to detect every case is to diff all files,
313 313 # which could be expensive.
314 314 # copyfile() carries over the permission, so the mode check could
315 315 # be in an 'elif' branch, but for the case where the file has
316 316 # changed without affecting mtime or size.
317 317 if (cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
318 318 or cpstat.st_size != st.st_size
319 319 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)):
320 320 ui.debug('file changed while diffing. '
321 321 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
322 322 util.copyfile(copy_fn, working_fn)
323 323
324 324 return 1
325 325 finally:
326 326 ui.note(_('cleaning up temp directory\n'))
327 327 shutil.rmtree(tmproot)
328 328
329 329 extdiffopts = [
330 330 ('o', 'option', [],
331 331 _('pass option to comparison program'), _('OPT')),
332 332 ('r', 'rev', [], _('revision'), _('REV')),
333 333 ('c', 'change', '', _('change made by revision'), _('REV')),
334 334 ('', 'patch', None, _('compare patches for two revisions'))
335 335 ] + cmdutil.walkopts + cmdutil.subrepoopts
336 336
337 337 @command('extdiff',
338 338 [('p', 'program', '', _('comparison program to run'), _('CMD')),
339 339 ] + extdiffopts,
340 340 _('hg extdiff [OPT]... [FILE]...'),
341 helpcategory=command.CATEGORY_FILE_CONTENTS,
341 342 inferrepo=True)
342 343 def extdiff(ui, repo, *pats, **opts):
343 344 '''use external program to diff repository (or selected files)
344 345
345 346 Show differences between revisions for the specified files, using
346 347 an external program. The default program used is diff, with
347 348 default options "-Npru".
348 349
349 350 To select a different program, use the -p/--program option. The
350 351 program will be passed the names of two directories to compare. To
351 352 pass additional options to the program, use -o/--option. These
352 353 will be passed before the names of the directories to compare.
353 354
354 355 When two revision arguments are given, then changes are shown
355 356 between those revisions. If only one revision is specified then
356 357 that revision is compared to the working directory, and, when no
357 358 revisions are specified, the working directory files are compared
358 359 to its parent.'''
359 360 opts = pycompat.byteskwargs(opts)
360 361 program = opts.get('program')
361 362 option = opts.get('option')
362 363 if not program:
363 364 program = 'diff'
364 365 option = option or ['-Npru']
365 366 cmdline = ' '.join(map(procutil.shellquote, [program] + option))
366 367 return dodiff(ui, repo, cmdline, pats, opts)
367 368
368 369 class savedcmd(object):
369 370 """use external program to diff repository (or selected files)
370 371
371 372 Show differences between revisions for the specified files, using
372 373 the following program::
373 374
374 375 %(path)s
375 376
376 377 When two revision arguments are given, then changes are shown
377 378 between those revisions. If only one revision is specified then
378 379 that revision is compared to the working directory, and, when no
379 380 revisions are specified, the working directory files are compared
380 381 to its parent.
381 382 """
382 383
383 384 def __init__(self, path, cmdline):
384 385 # We can't pass non-ASCII through docstrings (and path is
385 386 # in an unknown encoding anyway)
386 387 docpath = stringutil.escapestr(path)
387 388 self.__doc__ %= {r'path': pycompat.sysstr(stringutil.uirepr(docpath))}
388 389 self._cmdline = cmdline
389 390
390 391 def __call__(self, ui, repo, *pats, **opts):
391 392 opts = pycompat.byteskwargs(opts)
392 393 options = ' '.join(map(procutil.shellquote, opts['option']))
393 394 if options:
394 395 options = ' ' + options
395 396 return dodiff(ui, repo, self._cmdline + options, pats, opts)
396 397
397 398 def uisetup(ui):
398 399 for cmd, path in ui.configitems('extdiff'):
399 400 path = util.expandpath(path)
400 401 if cmd.startswith('cmd.'):
401 402 cmd = cmd[4:]
402 403 if not path:
403 404 path = procutil.findexe(cmd)
404 405 if path is None:
405 406 path = filemerge.findexternaltool(ui, cmd) or cmd
406 407 diffopts = ui.config('extdiff', 'opts.' + cmd)
407 408 cmdline = procutil.shellquote(path)
408 409 if diffopts:
409 410 cmdline += ' ' + diffopts
410 411 elif cmd.startswith('opts.'):
411 412 continue
412 413 else:
413 414 if path:
414 415 # case "cmd = path opts"
415 416 cmdline = path
416 417 diffopts = len(pycompat.shlexsplit(cmdline)) > 1
417 418 else:
418 419 # case "cmd ="
419 420 path = procutil.findexe(cmd)
420 421 if path is None:
421 422 path = filemerge.findexternaltool(ui, cmd) or cmd
422 423 cmdline = procutil.shellquote(path)
423 424 diffopts = False
424 425 # look for diff arguments in [diff-tools] then [merge-tools]
425 426 if not diffopts:
426 427 args = ui.config('diff-tools', cmd+'.diffargs') or \
427 428 ui.config('merge-tools', cmd+'.diffargs')
428 429 if args:
429 430 cmdline += ' ' + args
430 431 command(cmd, extdiffopts[:], _('hg %s [OPTION]... [FILE]...') % cmd,
431 432 inferrepo=True)(savedcmd(path, cmdline))
432 433
433 434 # tell hggettext to extract docstrings from these functions:
434 435 i18nfunctions = [savedcmd]
@@ -1,168 +1,169 b''
1 1 # fetch.py - pull and merge remote changes
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
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 '''pull, update and merge in one command (DEPRECATED)'''
9 9
10 10 from __future__ import absolute_import
11 11
12 12 from mercurial.i18n import _
13 13 from mercurial.node import (
14 14 short,
15 15 )
16 16 from mercurial import (
17 17 cmdutil,
18 18 error,
19 19 exchange,
20 20 hg,
21 21 lock,
22 22 pycompat,
23 23 registrar,
24 24 util,
25 25 )
26 26 from mercurial.utils import dateutil
27 27
28 28 release = lock.release
29 29 cmdtable = {}
30 30 command = registrar.command(cmdtable)
31 31 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
32 32 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
33 33 # be specifying the version(s) of Mercurial they are tested with, or
34 34 # leave the attribute unspecified.
35 35 testedwith = 'ships-with-hg-core'
36 36
37 37 @command('fetch',
38 38 [('r', 'rev', [],
39 39 _('a specific revision you would like to pull'), _('REV')),
40 40 ('', 'edit', None, _('invoke editor on commit messages')),
41 41 ('', 'force-editor', None, _('edit commit message (DEPRECATED)')),
42 42 ('', 'switch-parent', None, _('switch parents when merging')),
43 43 ] + cmdutil.commitopts + cmdutil.commitopts2 + cmdutil.remoteopts,
44 _('hg fetch [SOURCE]'))
44 _('hg fetch [SOURCE]'),
45 helpcategory=command.CATEGORY_REMOTE_REPO_MANAGEMENT)
45 46 def fetch(ui, repo, source='default', **opts):
46 47 '''pull changes from a remote repository, merge new changes if needed.
47 48
48 49 This finds all changes from the repository at the specified path
49 50 or URL and adds them to the local repository.
50 51
51 52 If the pulled changes add a new branch head, the head is
52 53 automatically merged, and the result of the merge is committed.
53 54 Otherwise, the working directory is updated to include the new
54 55 changes.
55 56
56 57 When a merge is needed, the working directory is first updated to
57 58 the newly pulled changes. Local changes are then merged into the
58 59 pulled changes. To switch the merge order, use --switch-parent.
59 60
60 61 See :hg:`help dates` for a list of formats valid for -d/--date.
61 62
62 63 Returns 0 on success.
63 64 '''
64 65
65 66 opts = pycompat.byteskwargs(opts)
66 67 date = opts.get('date')
67 68 if date:
68 69 opts['date'] = dateutil.parsedate(date)
69 70
70 71 parent, _p2 = repo.dirstate.parents()
71 72 branch = repo.dirstate.branch()
72 73 try:
73 74 branchnode = repo.branchtip(branch)
74 75 except error.RepoLookupError:
75 76 branchnode = None
76 77 if parent != branchnode:
77 78 raise error.Abort(_('working directory not at branch tip'),
78 79 hint=_("use 'hg update' to check out branch tip"))
79 80
80 81 wlock = lock = None
81 82 try:
82 83 wlock = repo.wlock()
83 84 lock = repo.lock()
84 85
85 86 cmdutil.bailifchanged(repo)
86 87
87 88 bheads = repo.branchheads(branch)
88 89 bheads = [head for head in bheads if len(repo[head].children()) == 0]
89 90 if len(bheads) > 1:
90 91 raise error.Abort(_('multiple heads in this branch '
91 92 '(use "hg heads ." and "hg merge" to merge)'))
92 93
93 94 other = hg.peer(repo, opts, ui.expandpath(source))
94 95 ui.status(_('pulling from %s\n') %
95 96 util.hidepassword(ui.expandpath(source)))
96 97 revs = None
97 98 if opts['rev']:
98 99 try:
99 100 revs = [other.lookup(rev) for rev in opts['rev']]
100 101 except error.CapabilityError:
101 102 err = _("other repository doesn't support revision lookup, "
102 103 "so a rev cannot be specified.")
103 104 raise error.Abort(err)
104 105
105 106 # Are there any changes at all?
106 107 modheads = exchange.pull(repo, other, heads=revs).cgresult
107 108 if modheads == 0:
108 109 return 0
109 110
110 111 # Is this a simple fast-forward along the current branch?
111 112 newheads = repo.branchheads(branch)
112 113 newchildren = repo.changelog.nodesbetween([parent], newheads)[2]
113 114 if len(newheads) == 1 and len(newchildren):
114 115 if newchildren[0] != parent:
115 116 return hg.update(repo, newchildren[0])
116 117 else:
117 118 return 0
118 119
119 120 # Are there more than one additional branch heads?
120 121 newchildren = [n for n in newchildren if n != parent]
121 122 newparent = parent
122 123 if newchildren:
123 124 newparent = newchildren[0]
124 125 hg.clean(repo, newparent)
125 126 newheads = [n for n in newheads if n != newparent]
126 127 if len(newheads) > 1:
127 128 ui.status(_('not merging with %d other new branch heads '
128 129 '(use "hg heads ." and "hg merge" to merge them)\n') %
129 130 (len(newheads) - 1))
130 131 return 1
131 132
132 133 if not newheads:
133 134 return 0
134 135
135 136 # Otherwise, let's merge.
136 137 err = False
137 138 if newheads:
138 139 # By default, we consider the repository we're pulling
139 140 # *from* as authoritative, so we merge our changes into
140 141 # theirs.
141 142 if opts['switch_parent']:
142 143 firstparent, secondparent = newparent, newheads[0]
143 144 else:
144 145 firstparent, secondparent = newheads[0], newparent
145 146 ui.status(_('updating to %d:%s\n') %
146 147 (repo.changelog.rev(firstparent),
147 148 short(firstparent)))
148 149 hg.clean(repo, firstparent)
149 150 ui.status(_('merging with %d:%s\n') %
150 151 (repo.changelog.rev(secondparent), short(secondparent)))
151 152 err = hg.merge(repo, secondparent, remind=False)
152 153
153 154 if not err:
154 155 # we don't translate commit messages
155 156 message = (cmdutil.logmessage(ui, opts) or
156 157 ('Automated merge with %s' %
157 158 util.removeauth(other.url())))
158 159 editopt = opts.get('edit') or opts.get('force_editor')
159 160 editor = cmdutil.getcommiteditor(edit=editopt, editform='fetch')
160 161 n = repo.commit(message, opts['user'], opts['date'], editor=editor)
161 162 ui.status(_('new changeset %d:%s merges remote changes '
162 163 'with local\n') % (repo.changelog.rev(n),
163 164 short(n)))
164 165
165 166 return err
166 167
167 168 finally:
168 169 release(lock, wlock)
@@ -1,615 +1,616 b''
1 1 # fix - rewrite file content in changesets and working copy
2 2 #
3 3 # Copyright 2018 Google LLC.
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 """rewrite file content in changesets or working copy (EXPERIMENTAL)
8 8
9 9 Provides a command that runs configured tools on the contents of modified files,
10 10 writing back any fixes to the working copy or replacing changesets.
11 11
12 12 Here is an example configuration that causes :hg:`fix` to apply automatic
13 13 formatting fixes to modified lines in C++ code::
14 14
15 15 [fix]
16 16 clang-format:command=clang-format --assume-filename={rootpath}
17 17 clang-format:linerange=--lines={first}:{last}
18 18 clang-format:fileset=set:**.cpp or **.hpp
19 19
20 20 The :command suboption forms the first part of the shell command that will be
21 21 used to fix a file. The content of the file is passed on standard input, and the
22 22 fixed file content is expected on standard output. If there is any output on
23 23 standard error, the file will not be affected. Some values may be substituted
24 24 into the command::
25 25
26 26 {rootpath} The path of the file being fixed, relative to the repo root
27 27 {basename} The name of the file being fixed, without the directory path
28 28
29 29 If the :linerange suboption is set, the tool will only be run if there are
30 30 changed lines in a file. The value of this suboption is appended to the shell
31 31 command once for every range of changed lines in the file. Some values may be
32 32 substituted into the command::
33 33
34 34 {first} The 1-based line number of the first line in the modified range
35 35 {last} The 1-based line number of the last line in the modified range
36 36
37 37 The :fileset suboption determines which files will be passed through each
38 38 configured tool. See :hg:`help fileset` for possible values. If there are file
39 39 arguments to :hg:`fix`, the intersection of these filesets is used.
40 40
41 41 There is also a configurable limit for the maximum size of file that will be
42 42 processed by :hg:`fix`::
43 43
44 44 [fix]
45 45 maxfilesize=2MB
46 46
47 47 """
48 48
49 49 from __future__ import absolute_import
50 50
51 51 import collections
52 52 import itertools
53 53 import os
54 54 import re
55 55 import subprocess
56 56
57 57 from mercurial.i18n import _
58 58 from mercurial.node import nullrev
59 59 from mercurial.node import wdirrev
60 60
61 61 from mercurial.utils import (
62 62 procutil,
63 63 )
64 64
65 65 from mercurial import (
66 66 cmdutil,
67 67 context,
68 68 copies,
69 69 error,
70 70 mdiff,
71 71 merge,
72 72 obsolete,
73 73 pycompat,
74 74 registrar,
75 75 scmutil,
76 76 util,
77 77 worker,
78 78 )
79 79
80 80 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
81 81 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
82 82 # be specifying the version(s) of Mercurial they are tested with, or
83 83 # leave the attribute unspecified.
84 84 testedwith = 'ships-with-hg-core'
85 85
86 86 cmdtable = {}
87 87 command = registrar.command(cmdtable)
88 88
89 89 configtable = {}
90 90 configitem = registrar.configitem(configtable)
91 91
92 92 # Register the suboptions allowed for each configured fixer.
93 93 FIXER_ATTRS = ('command', 'linerange', 'fileset')
94 94
95 95 for key in FIXER_ATTRS:
96 96 configitem('fix', '.*(:%s)?' % key, default=None, generic=True)
97 97
98 98 # A good default size allows most source code files to be fixed, but avoids
99 99 # letting fixer tools choke on huge inputs, which could be surprising to the
100 100 # user.
101 101 configitem('fix', 'maxfilesize', default='2MB')
102 102
103 103 allopt = ('', 'all', False, _('fix all non-public non-obsolete revisions'))
104 104 baseopt = ('', 'base', [], _('revisions to diff against (overrides automatic '
105 105 'selection, and applies to every revision being '
106 106 'fixed)'), _('REV'))
107 107 revopt = ('r', 'rev', [], _('revisions to fix'), _('REV'))
108 108 wdiropt = ('w', 'working-dir', False, _('fix the working directory'))
109 109 wholeopt = ('', 'whole', False, _('always fix every line of a file'))
110 110 usage = _('[OPTION]... [FILE]...')
111 111
112 @command('fix', [allopt, baseopt, revopt, wdiropt, wholeopt], usage)
112 @command('fix', [allopt, baseopt, revopt, wdiropt, wholeopt], usage,
113 helpcategory=command.CATEGORY_FILE_CONTENTS)
113 114 def fix(ui, repo, *pats, **opts):
114 115 """rewrite file content in changesets or working directory
115 116
116 117 Runs any configured tools to fix the content of files. Only affects files
117 118 with changes, unless file arguments are provided. Only affects changed lines
118 119 of files, unless the --whole flag is used. Some tools may always affect the
119 120 whole file regardless of --whole.
120 121
121 122 If revisions are specified with --rev, those revisions will be checked, and
122 123 they may be replaced with new revisions that have fixed file content. It is
123 124 desirable to specify all descendants of each specified revision, so that the
124 125 fixes propagate to the descendants. If all descendants are fixed at the same
125 126 time, no merging, rebasing, or evolution will be required.
126 127
127 128 If --working-dir is used, files with uncommitted changes in the working copy
128 129 will be fixed. If the checked-out revision is also fixed, the working
129 130 directory will update to the replacement revision.
130 131
131 132 When determining what lines of each file to fix at each revision, the whole
132 133 set of revisions being fixed is considered, so that fixes to earlier
133 134 revisions are not forgotten in later ones. The --base flag can be used to
134 135 override this default behavior, though it is not usually desirable to do so.
135 136 """
136 137 opts = pycompat.byteskwargs(opts)
137 138 if opts['all']:
138 139 if opts['rev']:
139 140 raise error.Abort(_('cannot specify both "--rev" and "--all"'))
140 141 opts['rev'] = ['not public() and not obsolete()']
141 142 opts['working_dir'] = True
142 143 with repo.wlock(), repo.lock(), repo.transaction('fix'):
143 144 revstofix = getrevstofix(ui, repo, opts)
144 145 basectxs = getbasectxs(repo, opts, revstofix)
145 146 workqueue, numitems = getworkqueue(ui, repo, pats, opts, revstofix,
146 147 basectxs)
147 148 fixers = getfixers(ui)
148 149
149 150 # There are no data dependencies between the workers fixing each file
150 151 # revision, so we can use all available parallelism.
151 152 def getfixes(items):
152 153 for rev, path in items:
153 154 ctx = repo[rev]
154 155 olddata = ctx[path].data()
155 156 newdata = fixfile(ui, opts, fixers, ctx, path, basectxs[rev])
156 157 # Don't waste memory/time passing unchanged content back, but
157 158 # produce one result per item either way.
158 159 yield (rev, path, newdata if newdata != olddata else None)
159 160 results = worker.worker(ui, 1.0, getfixes, tuple(), workqueue)
160 161
161 162 # We have to hold on to the data for each successor revision in memory
162 163 # until all its parents are committed. We ensure this by committing and
163 164 # freeing memory for the revisions in some topological order. This
164 165 # leaves a little bit of memory efficiency on the table, but also makes
165 166 # the tests deterministic. It might also be considered a feature since
166 167 # it makes the results more easily reproducible.
167 168 filedata = collections.defaultdict(dict)
168 169 replacements = {}
169 170 wdirwritten = False
170 171 commitorder = sorted(revstofix, reverse=True)
171 172 with ui.makeprogress(topic=_('fixing'), unit=_('files'),
172 173 total=sum(numitems.values())) as progress:
173 174 for rev, path, newdata in results:
174 175 progress.increment(item=path)
175 176 if newdata is not None:
176 177 filedata[rev][path] = newdata
177 178 numitems[rev] -= 1
178 179 # Apply the fixes for this and any other revisions that are
179 180 # ready and sitting at the front of the queue. Using a loop here
180 181 # prevents the queue from being blocked by the first revision to
181 182 # be ready out of order.
182 183 while commitorder and not numitems[commitorder[-1]]:
183 184 rev = commitorder.pop()
184 185 ctx = repo[rev]
185 186 if rev == wdirrev:
186 187 writeworkingdir(repo, ctx, filedata[rev], replacements)
187 188 wdirwritten = bool(filedata[rev])
188 189 else:
189 190 replacerev(ui, repo, ctx, filedata[rev], replacements)
190 191 del filedata[rev]
191 192
192 193 cleanup(repo, replacements, wdirwritten)
193 194
194 195 def cleanup(repo, replacements, wdirwritten):
195 196 """Calls scmutil.cleanupnodes() with the given replacements.
196 197
197 198 "replacements" is a dict from nodeid to nodeid, with one key and one value
198 199 for every revision that was affected by fixing. This is slightly different
199 200 from cleanupnodes().
200 201
201 202 "wdirwritten" is a bool which tells whether the working copy was affected by
202 203 fixing, since it has no entry in "replacements".
203 204
204 205 Useful as a hook point for extending "hg fix" with output summarizing the
205 206 effects of the command, though we choose not to output anything here.
206 207 """
207 208 replacements = {prec: [succ] for prec, succ in replacements.iteritems()}
208 209 scmutil.cleanupnodes(repo, replacements, 'fix', fixphase=True)
209 210
210 211 def getworkqueue(ui, repo, pats, opts, revstofix, basectxs):
211 212 """"Constructs the list of files to be fixed at specific revisions
212 213
213 214 It is up to the caller how to consume the work items, and the only
214 215 dependence between them is that replacement revisions must be committed in
215 216 topological order. Each work item represents a file in the working copy or
216 217 in some revision that should be fixed and written back to the working copy
217 218 or into a replacement revision.
218 219
219 220 Work items for the same revision are grouped together, so that a worker
220 221 pool starting with the first N items in parallel is likely to finish the
221 222 first revision's work before other revisions. This can allow us to write
222 223 the result to disk and reduce memory footprint. At time of writing, the
223 224 partition strategy in worker.py seems favorable to this. We also sort the
224 225 items by ascending revision number to match the order in which we commit
225 226 the fixes later.
226 227 """
227 228 workqueue = []
228 229 numitems = collections.defaultdict(int)
229 230 maxfilesize = ui.configbytes('fix', 'maxfilesize')
230 231 for rev in sorted(revstofix):
231 232 fixctx = repo[rev]
232 233 match = scmutil.match(fixctx, pats, opts)
233 234 for path in pathstofix(ui, repo, pats, opts, match, basectxs[rev],
234 235 fixctx):
235 236 if path not in fixctx:
236 237 continue
237 238 fctx = fixctx[path]
238 239 if fctx.islink():
239 240 continue
240 241 if fctx.size() > maxfilesize:
241 242 ui.warn(_('ignoring file larger than %s: %s\n') %
242 243 (util.bytecount(maxfilesize), path))
243 244 continue
244 245 workqueue.append((rev, path))
245 246 numitems[rev] += 1
246 247 return workqueue, numitems
247 248
248 249 def getrevstofix(ui, repo, opts):
249 250 """Returns the set of revision numbers that should be fixed"""
250 251 revs = set(scmutil.revrange(repo, opts['rev']))
251 252 for rev in revs:
252 253 checkfixablectx(ui, repo, repo[rev])
253 254 if revs:
254 255 cmdutil.checkunfinished(repo)
255 256 checknodescendants(repo, revs)
256 257 if opts.get('working_dir'):
257 258 revs.add(wdirrev)
258 259 if list(merge.mergestate.read(repo).unresolved()):
259 260 raise error.Abort('unresolved conflicts', hint="use 'hg resolve'")
260 261 if not revs:
261 262 raise error.Abort(
262 263 'no changesets specified', hint='use --rev or --working-dir')
263 264 return revs
264 265
265 266 def checknodescendants(repo, revs):
266 267 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
267 268 repo.revs('(%ld::) - (%ld)', revs, revs)):
268 269 raise error.Abort(_('can only fix a changeset together '
269 270 'with all its descendants'))
270 271
271 272 def checkfixablectx(ui, repo, ctx):
272 273 """Aborts if the revision shouldn't be replaced with a fixed one."""
273 274 if not ctx.mutable():
274 275 raise error.Abort('can\'t fix immutable changeset %s' %
275 276 (scmutil.formatchangeid(ctx),))
276 277 if ctx.obsolete():
277 278 # It would be better to actually check if the revision has a successor.
278 279 allowdivergence = ui.configbool('experimental',
279 280 'evolution.allowdivergence')
280 281 if not allowdivergence:
281 282 raise error.Abort('fixing obsolete revision could cause divergence')
282 283
283 284 def pathstofix(ui, repo, pats, opts, match, basectxs, fixctx):
284 285 """Returns the set of files that should be fixed in a context
285 286
286 287 The result depends on the base contexts; we include any file that has
287 288 changed relative to any of the base contexts. Base contexts should be
288 289 ancestors of the context being fixed.
289 290 """
290 291 files = set()
291 292 for basectx in basectxs:
292 293 stat = basectx.status(fixctx, match=match, listclean=bool(pats),
293 294 listunknown=bool(pats))
294 295 files.update(
295 296 set(itertools.chain(stat.added, stat.modified, stat.clean,
296 297 stat.unknown)))
297 298 return files
298 299
299 300 def lineranges(opts, path, basectxs, fixctx, content2):
300 301 """Returns the set of line ranges that should be fixed in a file
301 302
302 303 Of the form [(10, 20), (30, 40)].
303 304
304 305 This depends on the given base contexts; we must consider lines that have
305 306 changed versus any of the base contexts, and whether the file has been
306 307 renamed versus any of them.
307 308
308 309 Another way to understand this is that we exclude line ranges that are
309 310 common to the file in all base contexts.
310 311 """
311 312 if opts.get('whole'):
312 313 # Return a range containing all lines. Rely on the diff implementation's
313 314 # idea of how many lines are in the file, instead of reimplementing it.
314 315 return difflineranges('', content2)
315 316
316 317 rangeslist = []
317 318 for basectx in basectxs:
318 319 basepath = copies.pathcopies(basectx, fixctx).get(path, path)
319 320 if basepath in basectx:
320 321 content1 = basectx[basepath].data()
321 322 else:
322 323 content1 = ''
323 324 rangeslist.extend(difflineranges(content1, content2))
324 325 return unionranges(rangeslist)
325 326
326 327 def unionranges(rangeslist):
327 328 """Return the union of some closed intervals
328 329
329 330 >>> unionranges([])
330 331 []
331 332 >>> unionranges([(1, 100)])
332 333 [(1, 100)]
333 334 >>> unionranges([(1, 100), (1, 100)])
334 335 [(1, 100)]
335 336 >>> unionranges([(1, 100), (2, 100)])
336 337 [(1, 100)]
337 338 >>> unionranges([(1, 99), (1, 100)])
338 339 [(1, 100)]
339 340 >>> unionranges([(1, 100), (40, 60)])
340 341 [(1, 100)]
341 342 >>> unionranges([(1, 49), (50, 100)])
342 343 [(1, 100)]
343 344 >>> unionranges([(1, 48), (50, 100)])
344 345 [(1, 48), (50, 100)]
345 346 >>> unionranges([(1, 2), (3, 4), (5, 6)])
346 347 [(1, 6)]
347 348 """
348 349 rangeslist = sorted(set(rangeslist))
349 350 unioned = []
350 351 if rangeslist:
351 352 unioned, rangeslist = [rangeslist[0]], rangeslist[1:]
352 353 for a, b in rangeslist:
353 354 c, d = unioned[-1]
354 355 if a > d + 1:
355 356 unioned.append((a, b))
356 357 else:
357 358 unioned[-1] = (c, max(b, d))
358 359 return unioned
359 360
360 361 def difflineranges(content1, content2):
361 362 """Return list of line number ranges in content2 that differ from content1.
362 363
363 364 Line numbers are 1-based. The numbers are the first and last line contained
364 365 in the range. Single-line ranges have the same line number for the first and
365 366 last line. Excludes any empty ranges that result from lines that are only
366 367 present in content1. Relies on mdiff's idea of where the line endings are in
367 368 the string.
368 369
369 370 >>> from mercurial import pycompat
370 371 >>> lines = lambda s: b'\\n'.join([c for c in pycompat.iterbytestr(s)])
371 372 >>> difflineranges2 = lambda a, b: difflineranges(lines(a), lines(b))
372 373 >>> difflineranges2(b'', b'')
373 374 []
374 375 >>> difflineranges2(b'a', b'')
375 376 []
376 377 >>> difflineranges2(b'', b'A')
377 378 [(1, 1)]
378 379 >>> difflineranges2(b'a', b'a')
379 380 []
380 381 >>> difflineranges2(b'a', b'A')
381 382 [(1, 1)]
382 383 >>> difflineranges2(b'ab', b'')
383 384 []
384 385 >>> difflineranges2(b'', b'AB')
385 386 [(1, 2)]
386 387 >>> difflineranges2(b'abc', b'ac')
387 388 []
388 389 >>> difflineranges2(b'ab', b'aCb')
389 390 [(2, 2)]
390 391 >>> difflineranges2(b'abc', b'aBc')
391 392 [(2, 2)]
392 393 >>> difflineranges2(b'ab', b'AB')
393 394 [(1, 2)]
394 395 >>> difflineranges2(b'abcde', b'aBcDe')
395 396 [(2, 2), (4, 4)]
396 397 >>> difflineranges2(b'abcde', b'aBCDe')
397 398 [(2, 4)]
398 399 """
399 400 ranges = []
400 401 for lines, kind in mdiff.allblocks(content1, content2):
401 402 firstline, lastline = lines[2:4]
402 403 if kind == '!' and firstline != lastline:
403 404 ranges.append((firstline + 1, lastline))
404 405 return ranges
405 406
406 407 def getbasectxs(repo, opts, revstofix):
407 408 """Returns a map of the base contexts for each revision
408 409
409 410 The base contexts determine which lines are considered modified when we
410 411 attempt to fix just the modified lines in a file. It also determines which
411 412 files we attempt to fix, so it is important to compute this even when
412 413 --whole is used.
413 414 """
414 415 # The --base flag overrides the usual logic, and we give every revision
415 416 # exactly the set of baserevs that the user specified.
416 417 if opts.get('base'):
417 418 baserevs = set(scmutil.revrange(repo, opts.get('base')))
418 419 if not baserevs:
419 420 baserevs = {nullrev}
420 421 basectxs = {repo[rev] for rev in baserevs}
421 422 return {rev: basectxs for rev in revstofix}
422 423
423 424 # Proceed in topological order so that we can easily determine each
424 425 # revision's baserevs by looking at its parents and their baserevs.
425 426 basectxs = collections.defaultdict(set)
426 427 for rev in sorted(revstofix):
427 428 ctx = repo[rev]
428 429 for pctx in ctx.parents():
429 430 if pctx.rev() in basectxs:
430 431 basectxs[rev].update(basectxs[pctx.rev()])
431 432 else:
432 433 basectxs[rev].add(pctx)
433 434 return basectxs
434 435
435 436 def fixfile(ui, opts, fixers, fixctx, path, basectxs):
436 437 """Run any configured fixers that should affect the file in this context
437 438
438 439 Returns the file content that results from applying the fixers in some order
439 440 starting with the file's content in the fixctx. Fixers that support line
440 441 ranges will affect lines that have changed relative to any of the basectxs
441 442 (i.e. they will only avoid lines that are common to all basectxs).
442 443
443 444 A fixer tool's stdout will become the file's new content if and only if it
444 445 exits with code zero.
445 446 """
446 447 newdata = fixctx[path].data()
447 448 for fixername, fixer in fixers.iteritems():
448 449 if fixer.affects(opts, fixctx, path):
449 450 rangesfn = lambda: lineranges(opts, path, basectxs, fixctx, newdata)
450 451 command = fixer.command(ui, path, rangesfn)
451 452 if command is None:
452 453 continue
453 454 ui.debug('subprocess: %s\n' % (command,))
454 455 proc = subprocess.Popen(
455 456 procutil.tonativestr(command),
456 457 shell=True,
457 458 cwd=procutil.tonativestr(b'/'),
458 459 stdin=subprocess.PIPE,
459 460 stdout=subprocess.PIPE,
460 461 stderr=subprocess.PIPE)
461 462 newerdata, stderr = proc.communicate(newdata)
462 463 if stderr:
463 464 showstderr(ui, fixctx.rev(), fixername, stderr)
464 465 if proc.returncode == 0:
465 466 newdata = newerdata
466 467 elif not stderr:
467 468 showstderr(ui, fixctx.rev(), fixername,
468 469 _('exited with status %d\n') % (proc.returncode,))
469 470 return newdata
470 471
471 472 def showstderr(ui, rev, fixername, stderr):
472 473 """Writes the lines of the stderr string as warnings on the ui
473 474
474 475 Uses the revision number and fixername to give more context to each line of
475 476 the error message. Doesn't include file names, since those take up a lot of
476 477 space and would tend to be included in the error message if they were
477 478 relevant.
478 479 """
479 480 for line in re.split('[\r\n]+', stderr):
480 481 if line:
481 482 ui.warn(('['))
482 483 if rev is None:
483 484 ui.warn(_('wdir'), label='evolve.rev')
484 485 else:
485 486 ui.warn((str(rev)), label='evolve.rev')
486 487 ui.warn(('] %s: %s\n') % (fixername, line))
487 488
488 489 def writeworkingdir(repo, ctx, filedata, replacements):
489 490 """Write new content to the working copy and check out the new p1 if any
490 491
491 492 We check out a new revision if and only if we fixed something in both the
492 493 working directory and its parent revision. This avoids the need for a full
493 494 update/merge, and means that the working directory simply isn't affected
494 495 unless the --working-dir flag is given.
495 496
496 497 Directly updates the dirstate for the affected files.
497 498 """
498 499 for path, data in filedata.iteritems():
499 500 fctx = ctx[path]
500 501 fctx.write(data, fctx.flags())
501 502 if repo.dirstate[path] == 'n':
502 503 repo.dirstate.normallookup(path)
503 504
504 505 oldparentnodes = repo.dirstate.parents()
505 506 newparentnodes = [replacements.get(n, n) for n in oldparentnodes]
506 507 if newparentnodes != oldparentnodes:
507 508 repo.setparents(*newparentnodes)
508 509
509 510 def replacerev(ui, repo, ctx, filedata, replacements):
510 511 """Commit a new revision like the given one, but with file content changes
511 512
512 513 "ctx" is the original revision to be replaced by a modified one.
513 514
514 515 "filedata" is a dict that maps paths to their new file content. All other
515 516 paths will be recreated from the original revision without changes.
516 517 "filedata" may contain paths that didn't exist in the original revision;
517 518 they will be added.
518 519
519 520 "replacements" is a dict that maps a single node to a single node, and it is
520 521 updated to indicate the original revision is replaced by the newly created
521 522 one. No entry is added if the replacement's node already exists.
522 523
523 524 The new revision has the same parents as the old one, unless those parents
524 525 have already been replaced, in which case those replacements are the parents
525 526 of this new revision. Thus, if revisions are replaced in topological order,
526 527 there is no need to rebase them into the original topology later.
527 528 """
528 529
529 530 p1rev, p2rev = repo.changelog.parentrevs(ctx.rev())
530 531 p1ctx, p2ctx = repo[p1rev], repo[p2rev]
531 532 newp1node = replacements.get(p1ctx.node(), p1ctx.node())
532 533 newp2node = replacements.get(p2ctx.node(), p2ctx.node())
533 534
534 535 def filectxfn(repo, memctx, path):
535 536 if path not in ctx:
536 537 return None
537 538 fctx = ctx[path]
538 539 copied = fctx.renamed()
539 540 if copied:
540 541 copied = copied[0]
541 542 return context.memfilectx(
542 543 repo,
543 544 memctx,
544 545 path=fctx.path(),
545 546 data=filedata.get(path, fctx.data()),
546 547 islink=fctx.islink(),
547 548 isexec=fctx.isexec(),
548 549 copied=copied)
549 550
550 551 memctx = context.memctx(
551 552 repo,
552 553 parents=(newp1node, newp2node),
553 554 text=ctx.description(),
554 555 files=set(ctx.files()) | set(filedata.keys()),
555 556 filectxfn=filectxfn,
556 557 user=ctx.user(),
557 558 date=ctx.date(),
558 559 extra=ctx.extra(),
559 560 branch=ctx.branch(),
560 561 editor=None)
561 562 sucnode = memctx.commit()
562 563 prenode = ctx.node()
563 564 if prenode == sucnode:
564 565 ui.debug('node %s already existed\n' % (ctx.hex()))
565 566 else:
566 567 replacements[ctx.node()] = sucnode
567 568
568 569 def getfixers(ui):
569 570 """Returns a map of configured fixer tools indexed by their names
570 571
571 572 Each value is a Fixer object with methods that implement the behavior of the
572 573 fixer's config suboptions. Does not validate the config values.
573 574 """
574 575 result = {}
575 576 for name in fixernames(ui):
576 577 result[name] = Fixer()
577 578 attrs = ui.configsuboptions('fix', name)[1]
578 579 for key in FIXER_ATTRS:
579 580 setattr(result[name], pycompat.sysstr('_' + key),
580 581 attrs.get(key, ''))
581 582 return result
582 583
583 584 def fixernames(ui):
584 585 """Returns the names of [fix] config options that have suboptions"""
585 586 names = set()
586 587 for k, v in ui.configitems('fix'):
587 588 if ':' in k:
588 589 names.add(k.split(':', 1)[0])
589 590 return names
590 591
591 592 class Fixer(object):
592 593 """Wraps the raw config values for a fixer with methods"""
593 594
594 595 def affects(self, opts, fixctx, path):
595 596 """Should this fixer run on the file at the given path and context?"""
596 597 return scmutil.match(fixctx, [self._fileset], opts)(path)
597 598
598 599 def command(self, ui, path, rangesfn):
599 600 """A shell command to use to invoke this fixer on the given file/lines
600 601
601 602 May return None if there is no appropriate command to run for the given
602 603 parameters.
603 604 """
604 605 expand = cmdutil.rendercommandtemplate
605 606 parts = [expand(ui, self._command,
606 607 {'rootpath': path, 'basename': os.path.basename(path)})]
607 608 if self._linerange:
608 609 ranges = rangesfn()
609 610 if not ranges:
610 611 # No line ranges to fix, so don't run the fixer.
611 612 return None
612 613 for first, last in ranges:
613 614 parts.append(expand(ui, self._linerange,
614 615 {'first': first, 'last': last}))
615 616 return ' '.join(parts)
@@ -1,1089 +1,1090 b''
1 1 # githelp.py - Try to map Git commands to Mercurial equivalents.
2 2 #
3 3 # Copyright 2013 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 """try mapping git commands to Mercurial commands
8 8
9 9 Tries to map a given git command to a Mercurial command:
10 10
11 11 $ hg githelp -- git checkout master
12 12 hg update master
13 13
14 14 If an unknown command or parameter combination is detected, an error is
15 15 produced.
16 16 """
17 17
18 18 from __future__ import absolute_import
19 19
20 20 import getopt
21 21 import re
22 22
23 23 from mercurial.i18n import _
24 24 from mercurial import (
25 25 encoding,
26 26 error,
27 27 fancyopts,
28 28 registrar,
29 29 scmutil,
30 30 )
31 31 from mercurial.utils import (
32 32 procutil,
33 33 )
34 34
35 35 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
36 36 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
37 37 # be specifying the version(s) of Mercurial they are tested with, or
38 38 # leave the attribute unspecified.
39 39 testedwith = 'ships-with-hg-core'
40 40
41 41 cmdtable = {}
42 42 command = registrar.command(cmdtable)
43 43
44 44 def convert(s):
45 45 if s.startswith("origin/"):
46 46 return s[7:]
47 47 if 'HEAD' in s:
48 48 s = s.replace('HEAD', '.')
49 49 # HEAD~ in git is .~1 in mercurial
50 50 s = re.sub('~$', '~1', s)
51 51 return s
52 52
53 53 @command('^githelp|git', [
54 ], _('hg githelp'))
54 ], _('hg githelp'),
55 helpcategory=command.CATEGORY_HELP)
55 56 def githelp(ui, repo, *args, **kwargs):
56 57 '''suggests the Mercurial equivalent of the given git command
57 58
58 59 Usage: hg githelp -- <git command>
59 60 '''
60 61
61 62 if len(args) == 0 or (len(args) == 1 and args[0] =='git'):
62 63 raise error.Abort(_('missing git command - '
63 64 'usage: hg githelp -- <git command>'))
64 65
65 66 if args[0] == 'git':
66 67 args = args[1:]
67 68
68 69 cmd = args[0]
69 70 if not cmd in gitcommands:
70 71 raise error.Abort(_("error: unknown git command %s") % (cmd))
71 72
72 73 ui.pager('githelp')
73 74 args = args[1:]
74 75 return gitcommands[cmd](ui, repo, *args, **kwargs)
75 76
76 77 def parseoptions(ui, cmdoptions, args):
77 78 cmdoptions = list(cmdoptions)
78 79 opts = {}
79 80 args = list(args)
80 81 while True:
81 82 try:
82 83 args = fancyopts.fancyopts(list(args), cmdoptions, opts, True)
83 84 break
84 85 except getopt.GetoptError as ex:
85 86 flag = None
86 87 if "requires argument" in ex.msg:
87 88 raise
88 89 if ('--' + ex.opt) in ex.msg:
89 90 flag = '--' + ex.opt
90 91 elif ('-' + ex.opt) in ex.msg:
91 92 flag = '-' + ex.opt
92 93 else:
93 94 raise error.Abort(_("unknown option %s") % ex.opt)
94 95 try:
95 96 args.remove(flag)
96 97 except Exception:
97 98 msg = _("unknown option '%s' packed with other options")
98 99 hint = _("please try passing the option as its own flag: -%s")
99 100 raise error.Abort(msg % ex.opt, hint=hint % ex.opt)
100 101
101 102 ui.warn(_("ignoring unknown option %s\n") % flag)
102 103
103 104 args = list([convert(x) for x in args])
104 105 opts = dict([(k, convert(v)) if isinstance(v, str) else (k, v)
105 106 for k, v in opts.iteritems()])
106 107
107 108 return args, opts
108 109
109 110 class Command(object):
110 111 def __init__(self, name):
111 112 self.name = name
112 113 self.args = []
113 114 self.opts = {}
114 115
115 116 def __bytes__(self):
116 117 cmd = "hg " + self.name
117 118 if self.opts:
118 119 for k, values in sorted(self.opts.iteritems()):
119 120 for v in values:
120 121 if v:
121 122 cmd += " %s %s" % (k, v)
122 123 else:
123 124 cmd += " %s" % (k,)
124 125 if self.args:
125 126 cmd += " "
126 127 cmd += " ".join(self.args)
127 128 return cmd
128 129
129 130 __str__ = encoding.strmethod(__bytes__)
130 131
131 132 def append(self, value):
132 133 self.args.append(value)
133 134
134 135 def extend(self, values):
135 136 self.args.extend(values)
136 137
137 138 def __setitem__(self, key, value):
138 139 values = self.opts.setdefault(key, [])
139 140 values.append(value)
140 141
141 142 def __and__(self, other):
142 143 return AndCommand(self, other)
143 144
144 145 class AndCommand(object):
145 146 def __init__(self, left, right):
146 147 self.left = left
147 148 self.right = right
148 149
149 150 def __str__(self):
150 151 return "%s && %s" % (self.left, self.right)
151 152
152 153 def __and__(self, other):
153 154 return AndCommand(self, other)
154 155
155 156 def add(ui, repo, *args, **kwargs):
156 157 cmdoptions = [
157 158 ('A', 'all', None, ''),
158 159 ('p', 'patch', None, ''),
159 160 ]
160 161 args, opts = parseoptions(ui, cmdoptions, args)
161 162
162 163 if (opts.get('patch')):
163 164 ui.status(_("note: Mercurial will commit when complete, "
164 165 "as there is no staging area in Mercurial\n\n"))
165 166 cmd = Command('commit --interactive')
166 167 else:
167 168 cmd = Command("add")
168 169
169 170 if not opts.get('all'):
170 171 cmd.extend(args)
171 172 else:
172 173 ui.status(_("note: use hg addremove to remove files that have "
173 174 "been deleted\n\n"))
174 175
175 176 ui.status((bytes(cmd)), "\n")
176 177
177 178 def am(ui, repo, *args, **kwargs):
178 179 cmdoptions=[
179 180 ]
180 181 args, opts = parseoptions(ui, cmdoptions, args)
181 182 cmd = Command('import')
182 183 ui.status(bytes(cmd), "\n")
183 184
184 185 def apply(ui, repo, *args, **kwargs):
185 186 cmdoptions = [
186 187 ('p', 'p', int, ''),
187 188 ]
188 189 args, opts = parseoptions(ui, cmdoptions, args)
189 190
190 191 cmd = Command('import --no-commit')
191 192 if (opts.get('p')):
192 193 cmd['-p'] = opts.get('p')
193 194 cmd.extend(args)
194 195
195 196 ui.status((bytes(cmd)), "\n")
196 197
197 198 def bisect(ui, repo, *args, **kwargs):
198 199 ui.status(_("see 'hg help bisect' for how to use bisect\n\n"))
199 200
200 201 def blame(ui, repo, *args, **kwargs):
201 202 cmdoptions = [
202 203 ]
203 204 args, opts = parseoptions(ui, cmdoptions, args)
204 205 cmd = Command('annotate -udl')
205 206 cmd.extend([convert(v) for v in args])
206 207 ui.status((bytes(cmd)), "\n")
207 208
208 209 def branch(ui, repo, *args, **kwargs):
209 210 cmdoptions = [
210 211 ('', 'set-upstream', None, ''),
211 212 ('', 'set-upstream-to', '', ''),
212 213 ('d', 'delete', None, ''),
213 214 ('D', 'delete', None, ''),
214 215 ('m', 'move', None, ''),
215 216 ('M', 'move', None, ''),
216 217 ]
217 218 args, opts = parseoptions(ui, cmdoptions, args)
218 219
219 220 cmd = Command("bookmark")
220 221
221 222 if opts.get('set_upstream') or opts.get('set_upstream_to'):
222 223 ui.status(_("Mercurial has no concept of upstream branches\n"))
223 224 return
224 225 elif opts.get('delete'):
225 226 cmd = Command("strip")
226 227 for branch in args:
227 228 cmd['-B'] = branch
228 229 else:
229 230 cmd['-B'] = None
230 231 elif opts.get('move'):
231 232 if len(args) > 0:
232 233 if len(args) > 1:
233 234 old = args.pop(0)
234 235 else:
235 236 # shell command to output the active bookmark for the active
236 237 # revision
237 238 old = '`hg log -T"{activebookmark}" -r .`'
238 239 else:
239 240 raise error.Abort(_('missing newbranch argument'))
240 241 new = args[0]
241 242 cmd['-m'] = old
242 243 cmd.append(new)
243 244 else:
244 245 if len(args) > 1:
245 246 cmd['-r'] = args[1]
246 247 cmd.append(args[0])
247 248 elif len(args) == 1:
248 249 cmd.append(args[0])
249 250 ui.status((bytes(cmd)), "\n")
250 251
251 252 def ispath(repo, string):
252 253 """
253 254 The first argument to git checkout can either be a revision or a path. Let's
254 255 generally assume it's a revision, unless it's obviously a path. There are
255 256 too many ways to spell revisions in git for us to reasonably catch all of
256 257 them, so let's be conservative.
257 258 """
258 259 if scmutil.isrevsymbol(repo, string):
259 260 # if it's definitely a revision let's not even check if a file of the
260 261 # same name exists.
261 262 return False
262 263
263 264 cwd = repo.getcwd()
264 265 if cwd == '':
265 266 repopath = string
266 267 else:
267 268 repopath = cwd + '/' + string
268 269
269 270 exists = repo.wvfs.exists(repopath)
270 271 if exists:
271 272 return True
272 273
273 274 manifest = repo['.'].manifest()
274 275
275 276 didexist = (repopath in manifest) or manifest.hasdir(repopath)
276 277
277 278 return didexist
278 279
279 280 def checkout(ui, repo, *args, **kwargs):
280 281 cmdoptions = [
281 282 ('b', 'branch', '', ''),
282 283 ('B', 'branch', '', ''),
283 284 ('f', 'force', None, ''),
284 285 ('p', 'patch', None, ''),
285 286 ]
286 287 paths = []
287 288 if '--' in args:
288 289 sepindex = args.index('--')
289 290 paths.extend(args[sepindex + 1:])
290 291 args = args[:sepindex]
291 292
292 293 args, opts = parseoptions(ui, cmdoptions, args)
293 294
294 295 rev = None
295 296 if args and ispath(repo, args[0]):
296 297 paths = args + paths
297 298 elif args:
298 299 rev = args[0]
299 300 paths = args[1:] + paths
300 301
301 302 cmd = Command('update')
302 303
303 304 if opts.get('force'):
304 305 if paths or rev:
305 306 cmd['-C'] = None
306 307
307 308 if opts.get('patch'):
308 309 cmd = Command('revert')
309 310 cmd['-i'] = None
310 311
311 312 if opts.get('branch'):
312 313 if len(args) == 0:
313 314 cmd = Command('bookmark')
314 315 cmd.append(opts.get('branch'))
315 316 else:
316 317 cmd.append(args[0])
317 318 bookcmd = Command('bookmark')
318 319 bookcmd.append(opts.get('branch'))
319 320 cmd = cmd & bookcmd
320 321 # if there is any path argument supplied, use revert instead of update
321 322 elif len(paths) > 0:
322 323 ui.status(_("note: use --no-backup to avoid creating .orig files\n\n"))
323 324 cmd = Command('revert')
324 325 if opts.get('patch'):
325 326 cmd['-i'] = None
326 327 if rev:
327 328 cmd['-r'] = rev
328 329 cmd.extend(paths)
329 330 elif rev:
330 331 if opts.get('patch'):
331 332 cmd['-r'] = rev
332 333 else:
333 334 cmd.append(rev)
334 335 elif opts.get('force'):
335 336 cmd = Command('revert')
336 337 cmd['--all'] = None
337 338 else:
338 339 raise error.Abort(_("a commit must be specified"))
339 340
340 341 ui.status((bytes(cmd)), "\n")
341 342
342 343 def cherrypick(ui, repo, *args, **kwargs):
343 344 cmdoptions = [
344 345 ('', 'continue', None, ''),
345 346 ('', 'abort', None, ''),
346 347 ('e', 'edit', None, ''),
347 348 ]
348 349 args, opts = parseoptions(ui, cmdoptions, args)
349 350
350 351 cmd = Command('graft')
351 352
352 353 if opts.get('edit'):
353 354 cmd['--edit'] = None
354 355 if opts.get('continue'):
355 356 cmd['--continue'] = None
356 357 elif opts.get('abort'):
357 358 ui.status(_("note: hg graft does not have --abort\n\n"))
358 359 return
359 360 else:
360 361 cmd.extend(args)
361 362
362 363 ui.status((bytes(cmd)), "\n")
363 364
364 365 def clean(ui, repo, *args, **kwargs):
365 366 cmdoptions = [
366 367 ('d', 'd', None, ''),
367 368 ('f', 'force', None, ''),
368 369 ('x', 'x', None, ''),
369 370 ]
370 371 args, opts = parseoptions(ui, cmdoptions, args)
371 372
372 373 cmd = Command('purge')
373 374 if opts.get('x'):
374 375 cmd['--all'] = None
375 376 cmd.extend(args)
376 377
377 378 ui.status((bytes(cmd)), "\n")
378 379
379 380 def clone(ui, repo, *args, **kwargs):
380 381 cmdoptions = [
381 382 ('', 'bare', None, ''),
382 383 ('n', 'no-checkout', None, ''),
383 384 ('b', 'branch', '', ''),
384 385 ]
385 386 args, opts = parseoptions(ui, cmdoptions, args)
386 387
387 388 if len(args) == 0:
388 389 raise error.Abort(_("a repository to clone must be specified"))
389 390
390 391 cmd = Command('clone')
391 392 cmd.append(args[0])
392 393 if len(args) > 1:
393 394 cmd.append(args[1])
394 395
395 396 if opts.get('bare'):
396 397 cmd['-U'] = None
397 398 ui.status(_("note: Mercurial does not have bare clones. "
398 399 "-U will clone the repo without checking out a commit\n\n"))
399 400 elif opts.get('no_checkout'):
400 401 cmd['-U'] = None
401 402
402 403 if opts.get('branch'):
403 404 cocmd = Command("update")
404 405 cocmd.append(opts.get('branch'))
405 406 cmd = cmd & cocmd
406 407
407 408 ui.status((bytes(cmd)), "\n")
408 409
409 410 def commit(ui, repo, *args, **kwargs):
410 411 cmdoptions = [
411 412 ('a', 'all', None, ''),
412 413 ('m', 'message', '', ''),
413 414 ('p', 'patch', None, ''),
414 415 ('C', 'reuse-message', '', ''),
415 416 ('F', 'file', '', ''),
416 417 ('', 'author', '', ''),
417 418 ('', 'date', '', ''),
418 419 ('', 'amend', None, ''),
419 420 ('', 'no-edit', None, ''),
420 421 ]
421 422 args, opts = parseoptions(ui, cmdoptions, args)
422 423
423 424 cmd = Command('commit')
424 425 if opts.get('patch'):
425 426 cmd = Command('commit --interactive')
426 427
427 428 if opts.get('amend'):
428 429 if opts.get('no_edit'):
429 430 cmd = Command('amend')
430 431 else:
431 432 cmd['--amend'] = None
432 433
433 434 if opts.get('reuse_message'):
434 435 cmd['-M'] = opts.get('reuse_message')
435 436
436 437 if opts.get('message'):
437 438 cmd['-m'] = "'%s'" % (opts.get('message'),)
438 439
439 440 if opts.get('all'):
440 441 ui.status(_("note: Mercurial doesn't have a staging area, "
441 442 "so there is no --all. -A will add and remove files "
442 443 "for you though.\n\n"))
443 444
444 445 if opts.get('file'):
445 446 cmd['-l'] = opts.get('file')
446 447
447 448 if opts.get('author'):
448 449 cmd['-u'] = opts.get('author')
449 450
450 451 if opts.get('date'):
451 452 cmd['-d'] = opts.get('date')
452 453
453 454 cmd.extend(args)
454 455
455 456 ui.status((bytes(cmd)), "\n")
456 457
457 458 def deprecated(ui, repo, *args, **kwargs):
458 459 ui.warn(_('this command has been deprecated in the git project, '
459 460 'thus isn\'t supported by this tool\n\n'))
460 461
461 462 def diff(ui, repo, *args, **kwargs):
462 463 cmdoptions = [
463 464 ('a', 'all', None, ''),
464 465 ('', 'cached', None, ''),
465 466 ('R', 'reverse', None, ''),
466 467 ]
467 468 args, opts = parseoptions(ui, cmdoptions, args)
468 469
469 470 cmd = Command('diff')
470 471
471 472 if opts.get('cached'):
472 473 ui.status(_('note: Mercurial has no concept of a staging area, '
473 474 'so --cached does nothing\n\n'))
474 475
475 476 if opts.get('reverse'):
476 477 cmd['--reverse'] = None
477 478
478 479 for a in list(args):
479 480 args.remove(a)
480 481 try:
481 482 repo.revs(a)
482 483 cmd['-r'] = a
483 484 except Exception:
484 485 cmd.append(a)
485 486
486 487 ui.status((bytes(cmd)), "\n")
487 488
488 489 def difftool(ui, repo, *args, **kwargs):
489 490 ui.status(_('Mercurial does not enable external difftool by default. You '
490 491 'need to enable the extdiff extension in your .hgrc file by adding\n'
491 492 'extdiff =\n'
492 493 'to the [extensions] section and then running\n\n'
493 494 'hg extdiff -p <program>\n\n'
494 495 'See \'hg help extdiff\' and \'hg help -e extdiff\' for more '
495 496 'information.\n'))
496 497
497 498 def fetch(ui, repo, *args, **kwargs):
498 499 cmdoptions = [
499 500 ('', 'all', None, ''),
500 501 ('f', 'force', None, ''),
501 502 ]
502 503 args, opts = parseoptions(ui, cmdoptions, args)
503 504
504 505 cmd = Command('pull')
505 506
506 507 if len(args) > 0:
507 508 cmd.append(args[0])
508 509 if len(args) > 1:
509 510 ui.status(_("note: Mercurial doesn't have refspecs. "
510 511 "-r can be used to specify which commits you want to "
511 512 "pull. -B can be used to specify which bookmark you "
512 513 "want to pull.\n\n"))
513 514 for v in args[1:]:
514 515 if v in repo._bookmarks:
515 516 cmd['-B'] = v
516 517 else:
517 518 cmd['-r'] = v
518 519
519 520 ui.status((bytes(cmd)), "\n")
520 521
521 522 def grep(ui, repo, *args, **kwargs):
522 523 cmdoptions = [
523 524 ]
524 525 args, opts = parseoptions(ui, cmdoptions, args)
525 526
526 527 cmd = Command('grep')
527 528
528 529 # For basic usage, git grep and hg grep are the same. They both have the
529 530 # pattern first, followed by paths.
530 531 cmd.extend(args)
531 532
532 533 ui.status((bytes(cmd)), "\n")
533 534
534 535 def init(ui, repo, *args, **kwargs):
535 536 cmdoptions = [
536 537 ]
537 538 args, opts = parseoptions(ui, cmdoptions, args)
538 539
539 540 cmd = Command('init')
540 541
541 542 if len(args) > 0:
542 543 cmd.append(args[0])
543 544
544 545 ui.status((bytes(cmd)), "\n")
545 546
546 547 def log(ui, repo, *args, **kwargs):
547 548 cmdoptions = [
548 549 ('', 'follow', None, ''),
549 550 ('', 'decorate', None, ''),
550 551 ('n', 'number', '', ''),
551 552 ('1', '1', None, ''),
552 553 ('', 'pretty', '', ''),
553 554 ('', 'format', '', ''),
554 555 ('', 'oneline', None, ''),
555 556 ('', 'stat', None, ''),
556 557 ('', 'graph', None, ''),
557 558 ('p', 'patch', None, ''),
558 559 ]
559 560 args, opts = parseoptions(ui, cmdoptions, args)
560 561 ui.status(_('note: -v prints the entire commit message like Git does. To '
561 562 'print just the first line, drop the -v.\n\n'))
562 563 ui.status(_("note: see hg help revset for information on how to filter "
563 564 "log output\n\n"))
564 565
565 566 cmd = Command('log')
566 567 cmd['-v'] = None
567 568
568 569 if opts.get('number'):
569 570 cmd['-l'] = opts.get('number')
570 571 if opts.get('1'):
571 572 cmd['-l'] = '1'
572 573 if opts.get('stat'):
573 574 cmd['--stat'] = None
574 575 if opts.get('graph'):
575 576 cmd['-G'] = None
576 577 if opts.get('patch'):
577 578 cmd['-p'] = None
578 579
579 580 if opts.get('pretty') or opts.get('format') or opts.get('oneline'):
580 581 format = opts.get('format', '')
581 582 if 'format:' in format:
582 583 ui.status(_("note: --format format:??? equates to Mercurial's "
583 584 "--template. See hg help templates for more info.\n\n"))
584 585 cmd['--template'] = '???'
585 586 else:
586 587 ui.status(_("note: --pretty/format/oneline equate to Mercurial's "
587 588 "--style or --template. See hg help templates for "
588 589 "more info.\n\n"))
589 590 cmd['--style'] = '???'
590 591
591 592 if len(args) > 0:
592 593 if '..' in args[0]:
593 594 since, until = args[0].split('..')
594 595 cmd['-r'] = "'%s::%s'" % (since, until)
595 596 del args[0]
596 597 cmd.extend(args)
597 598
598 599 ui.status((bytes(cmd)), "\n")
599 600
600 601 def lsfiles(ui, repo, *args, **kwargs):
601 602 cmdoptions = [
602 603 ('c', 'cached', None, ''),
603 604 ('d', 'deleted', None, ''),
604 605 ('m', 'modified', None, ''),
605 606 ('o', 'others', None, ''),
606 607 ('i', 'ignored', None, ''),
607 608 ('s', 'stage', None, ''),
608 609 ('z', '_zero', None, ''),
609 610 ]
610 611 args, opts = parseoptions(ui, cmdoptions, args)
611 612
612 613 if (opts.get('modified') or opts.get('deleted')
613 614 or opts.get('others') or opts.get('ignored')):
614 615 cmd = Command('status')
615 616 if opts.get('deleted'):
616 617 cmd['-d'] = None
617 618 if opts.get('modified'):
618 619 cmd['-m'] = None
619 620 if opts.get('others'):
620 621 cmd['-o'] = None
621 622 if opts.get('ignored'):
622 623 cmd['-i'] = None
623 624 else:
624 625 cmd = Command('files')
625 626 if opts.get('stage'):
626 627 ui.status(_("note: Mercurial doesn't have a staging area, ignoring "
627 628 "--stage\n"))
628 629 if opts.get('_zero'):
629 630 cmd['-0'] = None
630 631 cmd.append('.')
631 632 for include in args:
632 633 cmd['-I'] = procutil.shellquote(include)
633 634
634 635 ui.status((bytes(cmd)), "\n")
635 636
636 637 def merge(ui, repo, *args, **kwargs):
637 638 cmdoptions = [
638 639 ]
639 640 args, opts = parseoptions(ui, cmdoptions, args)
640 641
641 642 cmd = Command('merge')
642 643
643 644 if len(args) > 0:
644 645 cmd.append(args[len(args) - 1])
645 646
646 647 ui.status((bytes(cmd)), "\n")
647 648
648 649 def mergebase(ui, repo, *args, **kwargs):
649 650 cmdoptions = []
650 651 args, opts = parseoptions(ui, cmdoptions, args)
651 652
652 653 if len(args) != 2:
653 654 args = ['A', 'B']
654 655
655 656 cmd = Command("log -T '{node}\\n' -r 'ancestor(%s,%s)'"
656 657 % (args[0], args[1]))
657 658
658 659 ui.status(_('note: ancestors() is part of the revset language\n'),
659 660 _("(learn more about revsets with 'hg help revsets')\n\n"))
660 661 ui.status((bytes(cmd)), "\n")
661 662
662 663 def mergetool(ui, repo, *args, **kwargs):
663 664 cmdoptions = []
664 665 args, opts = parseoptions(ui, cmdoptions, args)
665 666
666 667 cmd = Command("resolve")
667 668
668 669 if len(args) == 0:
669 670 cmd['--all'] = None
670 671 cmd.extend(args)
671 672 ui.status((bytes(cmd)), "\n")
672 673
673 674 def mv(ui, repo, *args, **kwargs):
674 675 cmdoptions = [
675 676 ('f', 'force', None, ''),
676 677 ]
677 678 args, opts = parseoptions(ui, cmdoptions, args)
678 679
679 680 cmd = Command('mv')
680 681 cmd.extend(args)
681 682
682 683 if opts.get('force'):
683 684 cmd['-f'] = None
684 685
685 686 ui.status((bytes(cmd)), "\n")
686 687
687 688 def pull(ui, repo, *args, **kwargs):
688 689 cmdoptions = [
689 690 ('', 'all', None, ''),
690 691 ('f', 'force', None, ''),
691 692 ('r', 'rebase', None, ''),
692 693 ]
693 694 args, opts = parseoptions(ui, cmdoptions, args)
694 695
695 696 cmd = Command('pull')
696 697 cmd['--rebase'] = None
697 698
698 699 if len(args) > 0:
699 700 cmd.append(args[0])
700 701 if len(args) > 1:
701 702 ui.status(_("note: Mercurial doesn't have refspecs. "
702 703 "-r can be used to specify which commits you want to "
703 704 "pull. -B can be used to specify which bookmark you "
704 705 "want to pull.\n\n"))
705 706 for v in args[1:]:
706 707 if v in repo._bookmarks:
707 708 cmd['-B'] = v
708 709 else:
709 710 cmd['-r'] = v
710 711
711 712 ui.status((bytes(cmd)), "\n")
712 713
713 714 def push(ui, repo, *args, **kwargs):
714 715 cmdoptions = [
715 716 ('', 'all', None, ''),
716 717 ('f', 'force', None, ''),
717 718 ]
718 719 args, opts = parseoptions(ui, cmdoptions, args)
719 720
720 721 cmd = Command('push')
721 722
722 723 if len(args) > 0:
723 724 cmd.append(args[0])
724 725 if len(args) > 1:
725 726 ui.status(_("note: Mercurial doesn't have refspecs. "
726 727 "-r can be used to specify which commits you want "
727 728 "to push. -B can be used to specify which bookmark "
728 729 "you want to push.\n\n"))
729 730 for v in args[1:]:
730 731 if v in repo._bookmarks:
731 732 cmd['-B'] = v
732 733 else:
733 734 cmd['-r'] = v
734 735
735 736 if opts.get('force'):
736 737 cmd['-f'] = None
737 738
738 739 ui.status((bytes(cmd)), "\n")
739 740
740 741 def rebase(ui, repo, *args, **kwargs):
741 742 cmdoptions = [
742 743 ('', 'all', None, ''),
743 744 ('i', 'interactive', None, ''),
744 745 ('', 'onto', '', ''),
745 746 ('', 'abort', None, ''),
746 747 ('', 'continue', None, ''),
747 748 ('', 'skip', None, ''),
748 749 ]
749 750 args, opts = parseoptions(ui, cmdoptions, args)
750 751
751 752 if opts.get('interactive'):
752 753 ui.status(_("note: hg histedit does not perform a rebase. "
753 754 "It just edits history.\n\n"))
754 755 cmd = Command('histedit')
755 756 if len(args) > 0:
756 757 ui.status(_("also note: 'hg histedit' will automatically detect"
757 758 " your stack, so no second argument is necessary\n\n"))
758 759 ui.status((bytes(cmd)), "\n")
759 760 return
760 761
761 762 if opts.get('skip'):
762 763 cmd = Command('revert --all -r .')
763 764 ui.status((bytes(cmd)), "\n")
764 765
765 766 cmd = Command('rebase')
766 767
767 768 if opts.get('continue') or opts.get('skip'):
768 769 cmd['--continue'] = None
769 770 if opts.get('abort'):
770 771 cmd['--abort'] = None
771 772
772 773 if opts.get('onto'):
773 774 ui.status(_("note: if you're trying to lift a commit off one branch, "
774 775 "try hg rebase -d <destination commit> -s <commit to be "
775 776 "lifted>\n\n"))
776 777 cmd['-d'] = convert(opts.get('onto'))
777 778 if len(args) < 2:
778 779 raise error.Abort(_("expected format: git rebase --onto X Y Z"))
779 780 cmd['-s'] = "'::%s - ::%s'" % (convert(args[1]), convert(args[0]))
780 781 else:
781 782 if len(args) == 1:
782 783 cmd['-d'] = convert(args[0])
783 784 elif len(args) == 2:
784 785 cmd['-d'] = convert(args[0])
785 786 cmd['-b'] = convert(args[1])
786 787
787 788 ui.status((bytes(cmd)), "\n")
788 789
789 790 def reflog(ui, repo, *args, **kwargs):
790 791 cmdoptions = [
791 792 ('', 'all', None, ''),
792 793 ]
793 794 args, opts = parseoptions(ui, cmdoptions, args)
794 795
795 796 cmd = Command('journal')
796 797 if opts.get('all'):
797 798 cmd['--all'] = None
798 799 if len(args) > 0:
799 800 cmd.append(args[0])
800 801
801 802 ui.status(bytes(cmd), "\n\n")
802 803 ui.status(_("note: in hg commits can be deleted from repo but we always"
803 804 " have backups\n"))
804 805
805 806 def reset(ui, repo, *args, **kwargs):
806 807 cmdoptions = [
807 808 ('', 'soft', None, ''),
808 809 ('', 'hard', None, ''),
809 810 ('', 'mixed', None, ''),
810 811 ]
811 812 args, opts = parseoptions(ui, cmdoptions, args)
812 813
813 814 commit = convert(args[0] if len(args) > 0 else '.')
814 815 hard = opts.get('hard')
815 816
816 817 if opts.get('mixed'):
817 818 ui.status(_('note: --mixed has no meaning since Mercurial has no '
818 819 'staging area\n\n'))
819 820 if opts.get('soft'):
820 821 ui.status(_('note: --soft has no meaning since Mercurial has no '
821 822 'staging area\n\n'))
822 823
823 824 cmd = Command('update')
824 825 if hard:
825 826 cmd.append('--clean')
826 827
827 828 cmd.append(commit)
828 829
829 830 ui.status((bytes(cmd)), "\n")
830 831
831 832 def revert(ui, repo, *args, **kwargs):
832 833 cmdoptions = [
833 834 ]
834 835 args, opts = parseoptions(ui, cmdoptions, args)
835 836
836 837 if len(args) > 1:
837 838 ui.status(_("note: hg backout doesn't support multiple commits at "
838 839 "once\n\n"))
839 840
840 841 cmd = Command('backout')
841 842 if args:
842 843 cmd.append(args[0])
843 844
844 845 ui.status((bytes(cmd)), "\n")
845 846
846 847 def revparse(ui, repo, *args, **kwargs):
847 848 cmdoptions = [
848 849 ('', 'show-cdup', None, ''),
849 850 ('', 'show-toplevel', None, ''),
850 851 ]
851 852 args, opts = parseoptions(ui, cmdoptions, args)
852 853
853 854 if opts.get('show_cdup') or opts.get('show_toplevel'):
854 855 cmd = Command('root')
855 856 if opts.get('show_cdup'):
856 857 ui.status(_("note: hg root prints the root of the repository\n\n"))
857 858 ui.status((bytes(cmd)), "\n")
858 859 else:
859 860 ui.status(_("note: see hg help revset for how to refer to commits\n"))
860 861
861 862 def rm(ui, repo, *args, **kwargs):
862 863 cmdoptions = [
863 864 ('f', 'force', None, ''),
864 865 ('n', 'dry-run', None, ''),
865 866 ]
866 867 args, opts = parseoptions(ui, cmdoptions, args)
867 868
868 869 cmd = Command('rm')
869 870 cmd.extend(args)
870 871
871 872 if opts.get('force'):
872 873 cmd['-f'] = None
873 874 if opts.get('dry_run'):
874 875 cmd['-n'] = None
875 876
876 877 ui.status((bytes(cmd)), "\n")
877 878
878 879 def show(ui, repo, *args, **kwargs):
879 880 cmdoptions = [
880 881 ('', 'name-status', None, ''),
881 882 ('', 'pretty', '', ''),
882 883 ('U', 'unified', int, ''),
883 884 ]
884 885 args, opts = parseoptions(ui, cmdoptions, args)
885 886
886 887 if opts.get('name_status'):
887 888 if opts.get('pretty') == 'format:':
888 889 cmd = Command('status')
889 890 cmd['--change'] = '.'
890 891 else:
891 892 cmd = Command('log')
892 893 cmd.append('--style status')
893 894 cmd.append('-r .')
894 895 elif len(args) > 0:
895 896 if ispath(repo, args[0]):
896 897 cmd = Command('cat')
897 898 else:
898 899 cmd = Command('export')
899 900 cmd.extend(args)
900 901 if opts.get('unified'):
901 902 cmd.append('--config diff.unified=%d' % (opts['unified'],))
902 903 elif opts.get('unified'):
903 904 cmd = Command('export')
904 905 cmd.append('--config diff.unified=%d' % (opts['unified'],))
905 906 else:
906 907 cmd = Command('export')
907 908
908 909 ui.status((bytes(cmd)), "\n")
909 910
910 911 def stash(ui, repo, *args, **kwargs):
911 912 cmdoptions = [
912 913 ]
913 914 args, opts = parseoptions(ui, cmdoptions, args)
914 915
915 916 cmd = Command('shelve')
916 917 action = args[0] if len(args) > 0 else None
917 918
918 919 if action == 'list':
919 920 cmd['-l'] = None
920 921 elif action == 'drop':
921 922 cmd['-d'] = None
922 923 if len(args) > 1:
923 924 cmd.append(args[1])
924 925 else:
925 926 cmd.append('<shelve name>')
926 927 elif action == 'pop' or action == 'apply':
927 928 cmd = Command('unshelve')
928 929 if len(args) > 1:
929 930 cmd.append(args[1])
930 931 if action == 'apply':
931 932 cmd['--keep'] = None
932 933 elif (action == 'branch' or action == 'show' or action == 'clear'
933 934 or action == 'create'):
934 935 ui.status(_("note: Mercurial doesn't have equivalents to the "
935 936 "git stash branch, show, clear, or create actions\n\n"))
936 937 return
937 938 else:
938 939 if len(args) > 0:
939 940 if args[0] != 'save':
940 941 cmd['--name'] = args[0]
941 942 elif len(args) > 1:
942 943 cmd['--name'] = args[1]
943 944
944 945 ui.status((bytes(cmd)), "\n")
945 946
946 947 def status(ui, repo, *args, **kwargs):
947 948 cmdoptions = [
948 949 ('', 'ignored', None, ''),
949 950 ]
950 951 args, opts = parseoptions(ui, cmdoptions, args)
951 952
952 953 cmd = Command('status')
953 954 cmd.extend(args)
954 955
955 956 if opts.get('ignored'):
956 957 cmd['-i'] = None
957 958
958 959 ui.status((bytes(cmd)), "\n")
959 960
960 961 def svn(ui, repo, *args, **kwargs):
961 962 if not args:
962 963 raise error.Abort(_('missing svn command'))
963 964 svncmd = args[0]
964 965 if svncmd not in gitsvncommands:
965 966 raise error.Abort(_('unknown git svn command "%s"') % (svncmd))
966 967
967 968 args = args[1:]
968 969 return gitsvncommands[svncmd](ui, repo, *args, **kwargs)
969 970
970 971 def svndcommit(ui, repo, *args, **kwargs):
971 972 cmdoptions = [
972 973 ]
973 974 args, opts = parseoptions(ui, cmdoptions, args)
974 975
975 976 cmd = Command('push')
976 977
977 978 ui.status((bytes(cmd)), "\n")
978 979
979 980 def svnfetch(ui, repo, *args, **kwargs):
980 981 cmdoptions = [
981 982 ]
982 983 args, opts = parseoptions(ui, cmdoptions, args)
983 984
984 985 cmd = Command('pull')
985 986 cmd.append('default-push')
986 987
987 988 ui.status((bytes(cmd)), "\n")
988 989
989 990 def svnfindrev(ui, repo, *args, **kwargs):
990 991 cmdoptions = [
991 992 ]
992 993 args, opts = parseoptions(ui, cmdoptions, args)
993 994
994 995 if not args:
995 996 raise error.Abort(_('missing find-rev argument'))
996 997
997 998 cmd = Command('log')
998 999 cmd['-r'] = args[0]
999 1000
1000 1001 ui.status((bytes(cmd)), "\n")
1001 1002
1002 1003 def svnrebase(ui, repo, *args, **kwargs):
1003 1004 cmdoptions = [
1004 1005 ('l', 'local', None, ''),
1005 1006 ]
1006 1007 args, opts = parseoptions(ui, cmdoptions, args)
1007 1008
1008 1009 pullcmd = Command('pull')
1009 1010 pullcmd.append('default-push')
1010 1011 rebasecmd = Command('rebase')
1011 1012 rebasecmd.append('tip')
1012 1013
1013 1014 cmd = pullcmd & rebasecmd
1014 1015
1015 1016 ui.status((bytes(cmd)), "\n")
1016 1017
1017 1018 def tag(ui, repo, *args, **kwargs):
1018 1019 cmdoptions = [
1019 1020 ('f', 'force', None, ''),
1020 1021 ('l', 'list', None, ''),
1021 1022 ('d', 'delete', None, ''),
1022 1023 ]
1023 1024 args, opts = parseoptions(ui, cmdoptions, args)
1024 1025
1025 1026 if opts.get('list'):
1026 1027 cmd = Command('tags')
1027 1028 else:
1028 1029 cmd = Command('tag')
1029 1030
1030 1031 if not args:
1031 1032 raise error.Abort(_('missing tag argument'))
1032 1033
1033 1034 cmd.append(args[0])
1034 1035 if len(args) > 1:
1035 1036 cmd['-r'] = args[1]
1036 1037
1037 1038 if opts.get('delete'):
1038 1039 cmd['--remove'] = None
1039 1040
1040 1041 if opts.get('force'):
1041 1042 cmd['-f'] = None
1042 1043
1043 1044 ui.status((bytes(cmd)), "\n")
1044 1045
1045 1046 gitcommands = {
1046 1047 'add': add,
1047 1048 'am': am,
1048 1049 'apply': apply,
1049 1050 'bisect': bisect,
1050 1051 'blame': blame,
1051 1052 'branch': branch,
1052 1053 'checkout': checkout,
1053 1054 'cherry-pick': cherrypick,
1054 1055 'clean': clean,
1055 1056 'clone': clone,
1056 1057 'commit': commit,
1057 1058 'diff': diff,
1058 1059 'difftool': difftool,
1059 1060 'fetch': fetch,
1060 1061 'grep': grep,
1061 1062 'init': init,
1062 1063 'log': log,
1063 1064 'ls-files': lsfiles,
1064 1065 'merge': merge,
1065 1066 'merge-base': mergebase,
1066 1067 'mergetool': mergetool,
1067 1068 'mv': mv,
1068 1069 'pull': pull,
1069 1070 'push': push,
1070 1071 'rebase': rebase,
1071 1072 'reflog': reflog,
1072 1073 'reset': reset,
1073 1074 'revert': revert,
1074 1075 'rev-parse': revparse,
1075 1076 'rm': rm,
1076 1077 'show': show,
1077 1078 'stash': stash,
1078 1079 'status': status,
1079 1080 'svn': svn,
1080 1081 'tag': tag,
1081 1082 'whatchanged': deprecated,
1082 1083 }
1083 1084
1084 1085 gitsvncommands = {
1085 1086 'dcommit': svndcommit,
1086 1087 'fetch': svnfetch,
1087 1088 'find-rev': svnfindrev,
1088 1089 'rebase': svnrebase,
1089 1090 }
@@ -1,329 +1,341 b''
1 1 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 '''commands to sign and verify changesets'''
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import binascii
11 11 import os
12 12
13 13 from mercurial.i18n import _
14 14 from mercurial import (
15 15 cmdutil,
16 16 error,
17 help,
17 18 match,
18 19 node as hgnode,
19 20 pycompat,
20 21 registrar,
21 22 )
22 23 from mercurial.utils import (
23 24 dateutil,
24 25 procutil,
25 26 )
26 27
27 28 cmdtable = {}
28 29 command = registrar.command(cmdtable)
29 30 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
30 31 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
31 32 # be specifying the version(s) of Mercurial they are tested with, or
32 33 # leave the attribute unspecified.
33 34 testedwith = 'ships-with-hg-core'
34 35
35 36 configtable = {}
36 37 configitem = registrar.configitem(configtable)
37 38
38 39 configitem('gpg', 'cmd',
39 40 default='gpg',
40 41 )
41 42 configitem('gpg', 'key',
42 43 default=None,
43 44 )
44 45 configitem('gpg', '.*',
45 46 default=None,
46 47 generic=True,
47 48 )
48 49
50 # Custom help category
51 _HELP_CATEGORY = 'gpg'
52
49 53 class gpg(object):
50 54 def __init__(self, path, key=None):
51 55 self.path = path
52 56 self.key = (key and " --local-user \"%s\"" % key) or ""
53 57
54 58 def sign(self, data):
55 59 gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
56 60 return procutil.filter(data, gpgcmd)
57 61
58 62 def verify(self, data, sig):
59 63 """ returns of the good and bad signatures"""
60 64 sigfile = datafile = None
61 65 try:
62 66 # create temporary files
63 67 fd, sigfile = pycompat.mkstemp(prefix="hg-gpg-", suffix=".sig")
64 68 fp = os.fdopen(fd, r'wb')
65 69 fp.write(sig)
66 70 fp.close()
67 71 fd, datafile = pycompat.mkstemp(prefix="hg-gpg-", suffix=".txt")
68 72 fp = os.fdopen(fd, r'wb')
69 73 fp.write(data)
70 74 fp.close()
71 75 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
72 76 "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
73 77 ret = procutil.filter("", gpgcmd)
74 78 finally:
75 79 for f in (sigfile, datafile):
76 80 try:
77 81 if f:
78 82 os.unlink(f)
79 83 except OSError:
80 84 pass
81 85 keys = []
82 86 key, fingerprint = None, None
83 87 for l in ret.splitlines():
84 88 # see DETAILS in the gnupg documentation
85 89 # filter the logger output
86 90 if not l.startswith("[GNUPG:]"):
87 91 continue
88 92 l = l[9:]
89 93 if l.startswith("VALIDSIG"):
90 94 # fingerprint of the primary key
91 95 fingerprint = l.split()[10]
92 96 elif l.startswith("ERRSIG"):
93 97 key = l.split(" ", 3)[:2]
94 98 key.append("")
95 99 fingerprint = None
96 100 elif (l.startswith("GOODSIG") or
97 101 l.startswith("EXPSIG") or
98 102 l.startswith("EXPKEYSIG") or
99 103 l.startswith("BADSIG")):
100 104 if key is not None:
101 105 keys.append(key + [fingerprint])
102 106 key = l.split(" ", 2)
103 107 fingerprint = None
104 108 if key is not None:
105 109 keys.append(key + [fingerprint])
106 110 return keys
107 111
108 112 def newgpg(ui, **opts):
109 113 """create a new gpg instance"""
110 114 gpgpath = ui.config("gpg", "cmd")
111 115 gpgkey = opts.get(r'key')
112 116 if not gpgkey:
113 117 gpgkey = ui.config("gpg", "key")
114 118 return gpg(gpgpath, gpgkey)
115 119
116 120 def sigwalk(repo):
117 121 """
118 122 walk over every sigs, yields a couple
119 123 ((node, version, sig), (filename, linenumber))
120 124 """
121 125 def parsefile(fileiter, context):
122 126 ln = 1
123 127 for l in fileiter:
124 128 if not l:
125 129 continue
126 130 yield (l.split(" ", 2), (context, ln))
127 131 ln += 1
128 132
129 133 # read the heads
130 134 fl = repo.file(".hgsigs")
131 135 for r in reversed(fl.heads()):
132 136 fn = ".hgsigs|%s" % hgnode.short(r)
133 137 for item in parsefile(fl.read(r).splitlines(), fn):
134 138 yield item
135 139 try:
136 140 # read local signatures
137 141 fn = "localsigs"
138 142 for item in parsefile(repo.vfs(fn), fn):
139 143 yield item
140 144 except IOError:
141 145 pass
142 146
143 147 def getkeys(ui, repo, mygpg, sigdata, context):
144 148 """get the keys who signed a data"""
145 149 fn, ln = context
146 150 node, version, sig = sigdata
147 151 prefix = "%s:%d" % (fn, ln)
148 152 node = hgnode.bin(node)
149 153
150 154 data = node2txt(repo, node, version)
151 155 sig = binascii.a2b_base64(sig)
152 156 keys = mygpg.verify(data, sig)
153 157
154 158 validkeys = []
155 159 # warn for expired key and/or sigs
156 160 for key in keys:
157 161 if key[0] == "ERRSIG":
158 162 ui.write(_("%s Unknown key ID \"%s\"\n") % (prefix, key[1]))
159 163 continue
160 164 if key[0] == "BADSIG":
161 165 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
162 166 continue
163 167 if key[0] == "EXPSIG":
164 168 ui.write(_("%s Note: Signature has expired"
165 169 " (signed by: \"%s\")\n") % (prefix, key[2]))
166 170 elif key[0] == "EXPKEYSIG":
167 171 ui.write(_("%s Note: This key has expired"
168 172 " (signed by: \"%s\")\n") % (prefix, key[2]))
169 173 validkeys.append((key[1], key[2], key[3]))
170 174 return validkeys
171 175
172 @command("sigs", [], _('hg sigs'))
176 @command("sigs", [], _('hg sigs'), helpcategory=_HELP_CATEGORY)
173 177 def sigs(ui, repo):
174 178 """list signed changesets"""
175 179 mygpg = newgpg(ui)
176 180 revs = {}
177 181
178 182 for data, context in sigwalk(repo):
179 183 node, version, sig = data
180 184 fn, ln = context
181 185 try:
182 186 n = repo.lookup(node)
183 187 except KeyError:
184 188 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
185 189 continue
186 190 r = repo.changelog.rev(n)
187 191 keys = getkeys(ui, repo, mygpg, data, context)
188 192 if not keys:
189 193 continue
190 194 revs.setdefault(r, [])
191 195 revs[r].extend(keys)
192 196 for rev in sorted(revs, reverse=True):
193 197 for k in revs[rev]:
194 198 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
195 199 ui.write("%-30s %s\n" % (keystr(ui, k), r))
196 200
197 @command("sigcheck", [], _('hg sigcheck REV'))
201 @command("sigcheck", [], _('hg sigcheck REV'), helpcategory=_HELP_CATEGORY)
198 202 def sigcheck(ui, repo, rev):
199 203 """verify all the signatures there may be for a particular revision"""
200 204 mygpg = newgpg(ui)
201 205 rev = repo.lookup(rev)
202 206 hexrev = hgnode.hex(rev)
203 207 keys = []
204 208
205 209 for data, context in sigwalk(repo):
206 210 node, version, sig = data
207 211 if node == hexrev:
208 212 k = getkeys(ui, repo, mygpg, data, context)
209 213 if k:
210 214 keys.extend(k)
211 215
212 216 if not keys:
213 217 ui.write(_("no valid signature for %s\n") % hgnode.short(rev))
214 218 return
215 219
216 220 # print summary
217 221 ui.write(_("%s is signed by:\n") % hgnode.short(rev))
218 222 for key in keys:
219 223 ui.write(" %s\n" % keystr(ui, key))
220 224
221 225 def keystr(ui, key):
222 226 """associate a string to a key (username, comment)"""
223 227 keyid, user, fingerprint = key
224 228 comment = ui.config("gpg", fingerprint)
225 229 if comment:
226 230 return "%s (%s)" % (user, comment)
227 231 else:
228 232 return user
229 233
230 234 @command("sign",
231 235 [('l', 'local', None, _('make the signature local')),
232 236 ('f', 'force', None, _('sign even if the sigfile is modified')),
233 237 ('', 'no-commit', None, _('do not commit the sigfile after signing')),
234 238 ('k', 'key', '',
235 239 _('the key id to sign with'), _('ID')),
236 240 ('m', 'message', '',
237 241 _('use text as commit message'), _('TEXT')),
238 242 ('e', 'edit', False, _('invoke editor on commit messages')),
239 243 ] + cmdutil.commitopts2,
240 _('hg sign [OPTION]... [REV]...'))
244 _('hg sign [OPTION]... [REV]...'),
245 helpcategory=_HELP_CATEGORY)
241 246 def sign(ui, repo, *revs, **opts):
242 247 """add a signature for the current or given revision
243 248
244 249 If no revision is given, the parent of the working directory is used,
245 250 or tip if no revision is checked out.
246 251
247 252 The ``gpg.cmd`` config setting can be used to specify the command
248 253 to run. A default key can be specified with ``gpg.key``.
249 254
250 255 See :hg:`help dates` for a list of formats valid for -d/--date.
251 256 """
252 257 with repo.wlock():
253 258 return _dosign(ui, repo, *revs, **opts)
254 259
255 260 def _dosign(ui, repo, *revs, **opts):
256 261 mygpg = newgpg(ui, **opts)
257 262 opts = pycompat.byteskwargs(opts)
258 263 sigver = "0"
259 264 sigmessage = ""
260 265
261 266 date = opts.get('date')
262 267 if date:
263 268 opts['date'] = dateutil.parsedate(date)
264 269
265 270 if revs:
266 271 nodes = [repo.lookup(n) for n in revs]
267 272 else:
268 273 nodes = [node for node in repo.dirstate.parents()
269 274 if node != hgnode.nullid]
270 275 if len(nodes) > 1:
271 276 raise error.Abort(_('uncommitted merge - please provide a '
272 277 'specific revision'))
273 278 if not nodes:
274 279 nodes = [repo.changelog.tip()]
275 280
276 281 for n in nodes:
277 282 hexnode = hgnode.hex(n)
278 283 ui.write(_("signing %d:%s\n") % (repo.changelog.rev(n),
279 284 hgnode.short(n)))
280 285 # build data
281 286 data = node2txt(repo, n, sigver)
282 287 sig = mygpg.sign(data)
283 288 if not sig:
284 289 raise error.Abort(_("error while signing"))
285 290 sig = binascii.b2a_base64(sig)
286 291 sig = sig.replace("\n", "")
287 292 sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
288 293
289 294 # write it
290 295 if opts['local']:
291 296 repo.vfs.append("localsigs", sigmessage)
292 297 return
293 298
294 299 if not opts["force"]:
295 300 msigs = match.exact(repo.root, '', ['.hgsigs'])
296 301 if any(repo.status(match=msigs, unknown=True, ignored=True)):
297 302 raise error.Abort(_("working copy of .hgsigs is changed "),
298 303 hint=_("please commit .hgsigs manually"))
299 304
300 305 sigsfile = repo.wvfs(".hgsigs", "ab")
301 306 sigsfile.write(sigmessage)
302 307 sigsfile.close()
303 308
304 309 if '.hgsigs' not in repo.dirstate:
305 310 repo[None].add([".hgsigs"])
306 311
307 312 if opts["no_commit"]:
308 313 return
309 314
310 315 message = opts['message']
311 316 if not message:
312 317 # we don't translate commit messages
313 318 message = "\n".join(["Added signature for changeset %s"
314 319 % hgnode.short(n)
315 320 for n in nodes])
316 321 try:
317 322 editor = cmdutil.getcommiteditor(editform='gpg.sign',
318 323 **pycompat.strkwargs(opts))
319 324 repo.commit(message, opts['user'], opts['date'], match=msigs,
320 325 editor=editor)
321 326 except ValueError as inst:
322 327 raise error.Abort(pycompat.bytestr(inst))
323 328
324 329 def node2txt(repo, node, ver):
325 330 """map a manifest into some text"""
326 331 if ver == "0":
327 332 return "%s\n" % hgnode.hex(node)
328 333 else:
329 334 raise error.Abort(_("unknown signature version"))
335
336 def extsetup(ui):
337 # Add our category before "Repository maintenance".
338 help.CATEGORY_ORDER.insert(
339 help.CATEGORY_ORDER.index(command.CATEGORY_MAINTENANCE),
340 _HELP_CATEGORY)
341 help.CATEGORY_NAMES[_HELP_CATEGORY] = 'GPG signing'
@@ -1,70 +1,71 b''
1 1 # ASCII graph log extension for Mercurial
2 2 #
3 3 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
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 '''command to view revision graphs from a shell (DEPRECATED)
9 9
10 10 The functionality of this extension has been include in core Mercurial
11 11 since version 2.3. Please use :hg:`log -G ...` instead.
12 12
13 13 This extension adds a --graph option to the incoming, outgoing and log
14 14 commands. When this options is given, an ASCII representation of the
15 15 revision graph is also shown.
16 16 '''
17 17
18 18 from __future__ import absolute_import
19 19
20 20 from mercurial.i18n import _
21 21 from mercurial import (
22 22 cmdutil,
23 23 commands,
24 24 registrar,
25 25 )
26 26
27 27 cmdtable = {}
28 28 command = registrar.command(cmdtable)
29 29 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
30 30 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
31 31 # be specifying the version(s) of Mercurial they are tested with, or
32 32 # leave the attribute unspecified.
33 33 testedwith = 'ships-with-hg-core'
34 34
35 35 @command('glog',
36 36 [('f', 'follow', None,
37 37 _('follow changeset history, or file history across copies and renames')),
38 38 ('', 'follow-first', None,
39 39 _('only follow the first parent of merge changesets (DEPRECATED)')),
40 40 ('d', 'date', '', _('show revisions matching date spec'), _('DATE')),
41 41 ('C', 'copies', None, _('show copied files')),
42 42 ('k', 'keyword', [],
43 43 _('do case-insensitive search for a given text'), _('TEXT')),
44 44 ('r', 'rev', [], _('show the specified revision or revset'), _('REV')),
45 45 ('', 'removed', None, _('include revisions where files were removed')),
46 46 ('m', 'only-merges', None, _('show only merges (DEPRECATED)')),
47 47 ('u', 'user', [], _('revisions committed by user'), _('USER')),
48 48 ('', 'only-branch', [],
49 49 _('show only changesets within the given named branch (DEPRECATED)'),
50 50 _('BRANCH')),
51 51 ('b', 'branch', [],
52 52 _('show changesets within the given named branch'), _('BRANCH')),
53 53 ('P', 'prune', [],
54 54 _('do not display revision or any of its ancestors'), _('REV')),
55 55 ] + cmdutil.logopts + cmdutil.walkopts,
56 56 _('[OPTION]... [FILE]'),
57 helpcategory=command.CATEGORY_CHANGE_NAVIGATION,
57 58 inferrepo=True)
58 59 def glog(ui, repo, *pats, **opts):
59 60 """show revision history alongside an ASCII revision graph
60 61
61 62 Print a revision history alongside a revision graph drawn with
62 63 ASCII characters.
63 64
64 65 Nodes printed as an @ character are parents of the working
65 66 directory.
66 67
67 68 This is an alias to :hg:`log -G`.
68 69 """
69 70 opts[r'graph'] = True
70 71 return commands.log(ui, repo, *pats, **opts)
@@ -1,359 +1,360 b''
1 1 # Minimal support for git commands on an hg repository
2 2 #
3 3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
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 '''browse the repository in a graphical way
9 9
10 10 The hgk extension allows browsing the history of a repository in a
11 11 graphical way. It requires Tcl/Tk version 8.4 or later. (Tcl/Tk is not
12 12 distributed with Mercurial.)
13 13
14 14 hgk consists of two parts: a Tcl script that does the displaying and
15 15 querying of information, and an extension to Mercurial named hgk.py,
16 16 which provides hooks for hgk to get information. hgk can be found in
17 17 the contrib directory, and the extension is shipped in the hgext
18 18 repository, and needs to be enabled.
19 19
20 20 The :hg:`view` command will launch the hgk Tcl script. For this command
21 21 to work, hgk must be in your search path. Alternately, you can specify
22 22 the path to hgk in your configuration file::
23 23
24 24 [hgk]
25 25 path = /location/of/hgk
26 26
27 27 hgk can make use of the extdiff extension to visualize revisions.
28 28 Assuming you had already configured extdiff vdiff command, just add::
29 29
30 30 [hgk]
31 31 vdiff=vdiff
32 32
33 33 Revisions context menu will now display additional entries to fire
34 34 vdiff on hovered and selected revisions.
35 35 '''
36 36
37 37 from __future__ import absolute_import
38 38
39 39 import os
40 40
41 41 from mercurial.i18n import _
42 42 from mercurial.node import (
43 43 nullid,
44 44 nullrev,
45 45 short,
46 46 )
47 47 from mercurial import (
48 48 commands,
49 49 obsolete,
50 50 patch,
51 51 pycompat,
52 52 registrar,
53 53 scmutil,
54 54 )
55 55
56 56 cmdtable = {}
57 57 command = registrar.command(cmdtable)
58 58 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
59 59 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
60 60 # be specifying the version(s) of Mercurial they are tested with, or
61 61 # leave the attribute unspecified.
62 62 testedwith = 'ships-with-hg-core'
63 63
64 64 configtable = {}
65 65 configitem = registrar.configitem(configtable)
66 66
67 67 configitem('hgk', 'path',
68 68 default='hgk',
69 69 )
70 70
71 71 @command('debug-diff-tree',
72 72 [('p', 'patch', None, _('generate patch')),
73 73 ('r', 'recursive', None, _('recursive')),
74 74 ('P', 'pretty', None, _('pretty')),
75 75 ('s', 'stdin', None, _('stdin')),
76 76 ('C', 'copy', None, _('detect copies')),
77 77 ('S', 'search', "", _('search'))],
78 78 ('[OPTION]... NODE1 NODE2 [FILE]...'),
79 79 inferrepo=True)
80 80 def difftree(ui, repo, node1=None, node2=None, *files, **opts):
81 81 """diff trees from two commits"""
82 82
83 83 def __difftree(repo, node1, node2, files=None):
84 84 assert node2 is not None
85 85 if files is None:
86 86 files = []
87 87 mmap = repo[node1].manifest()
88 88 mmap2 = repo[node2].manifest()
89 89 m = scmutil.match(repo[node1], files)
90 90 modified, added, removed = repo.status(node1, node2, m)[:3]
91 91 empty = short(nullid)
92 92
93 93 for f in modified:
94 94 # TODO get file permissions
95 95 ui.write((":100664 100664 %s %s M\t%s\t%s\n") %
96 96 (short(mmap[f]), short(mmap2[f]), f, f))
97 97 for f in added:
98 98 ui.write((":000000 100664 %s %s N\t%s\t%s\n") %
99 99 (empty, short(mmap2[f]), f, f))
100 100 for f in removed:
101 101 ui.write((":100664 000000 %s %s D\t%s\t%s\n") %
102 102 (short(mmap[f]), empty, f, f))
103 103 ##
104 104
105 105 while True:
106 106 if opts[r'stdin']:
107 107 line = ui.fin.readline()
108 108 if not line:
109 109 break
110 110 line = line.rstrip(pycompat.oslinesep).split(b' ')
111 111 node1 = line[0]
112 112 if len(line) > 1:
113 113 node2 = line[1]
114 114 else:
115 115 node2 = None
116 116 node1 = repo.lookup(node1)
117 117 if node2:
118 118 node2 = repo.lookup(node2)
119 119 else:
120 120 node2 = node1
121 121 node1 = repo.changelog.parents(node1)[0]
122 122 if opts[r'patch']:
123 123 if opts[r'pretty']:
124 124 catcommit(ui, repo, node2, "")
125 125 m = scmutil.match(repo[node1], files)
126 126 diffopts = patch.difffeatureopts(ui)
127 127 diffopts.git = True
128 128 chunks = patch.diff(repo, node1, node2, match=m,
129 129 opts=diffopts)
130 130 for chunk in chunks:
131 131 ui.write(chunk)
132 132 else:
133 133 __difftree(repo, node1, node2, files=files)
134 134 if not opts[r'stdin']:
135 135 break
136 136
137 137 def catcommit(ui, repo, n, prefix, ctx=None):
138 138 nlprefix = '\n' + prefix
139 139 if ctx is None:
140 140 ctx = repo[n]
141 141 # use ctx.node() instead ??
142 142 ui.write(("tree %s\n" % short(ctx.changeset()[0])))
143 143 for p in ctx.parents():
144 144 ui.write(("parent %s\n" % p))
145 145
146 146 date = ctx.date()
147 147 description = ctx.description().replace("\0", "")
148 148 ui.write(("author %s %d %d\n" % (ctx.user(), int(date[0]), date[1])))
149 149
150 150 if 'committer' in ctx.extra():
151 151 ui.write(("committer %s\n" % ctx.extra()['committer']))
152 152
153 153 ui.write(("revision %d\n" % ctx.rev()))
154 154 ui.write(("branch %s\n" % ctx.branch()))
155 155 if obsolete.isenabled(repo, obsolete.createmarkersopt):
156 156 if ctx.obsolete():
157 157 ui.write(("obsolete\n"))
158 158 ui.write(("phase %s\n\n" % ctx.phasestr()))
159 159
160 160 if prefix != "":
161 161 ui.write("%s%s\n" % (prefix,
162 162 description.replace('\n', nlprefix).strip()))
163 163 else:
164 164 ui.write(description + "\n")
165 165 if prefix:
166 166 ui.write('\0')
167 167
168 168 @command('debug-merge-base', [], _('REV REV'))
169 169 def base(ui, repo, node1, node2):
170 170 """output common ancestor information"""
171 171 node1 = repo.lookup(node1)
172 172 node2 = repo.lookup(node2)
173 173 n = repo.changelog.ancestor(node1, node2)
174 174 ui.write(short(n) + "\n")
175 175
176 176 @command('debug-cat-file',
177 177 [('s', 'stdin', None, _('stdin'))],
178 178 _('[OPTION]... TYPE FILE'),
179 179 inferrepo=True)
180 180 def catfile(ui, repo, type=None, r=None, **opts):
181 181 """cat a specific revision"""
182 182 # in stdin mode, every line except the commit is prefixed with two
183 183 # spaces. This way the our caller can find the commit without magic
184 184 # strings
185 185 #
186 186 prefix = ""
187 187 if opts[r'stdin']:
188 188 line = ui.fin.readline()
189 189 if not line:
190 190 return
191 191 (type, r) = line.rstrip(pycompat.oslinesep).split(b' ')
192 192 prefix = " "
193 193 else:
194 194 if not type or not r:
195 195 ui.warn(_("cat-file: type or revision not supplied\n"))
196 196 commands.help_(ui, 'cat-file')
197 197
198 198 while r:
199 199 if type != "commit":
200 200 ui.warn(_("aborting hg cat-file only understands commits\n"))
201 201 return 1
202 202 n = repo.lookup(r)
203 203 catcommit(ui, repo, n, prefix)
204 204 if opts[r'stdin']:
205 205 line = ui.fin.readline()
206 206 if not line:
207 207 break
208 208 (type, r) = line.rstrip(pycompat.oslinesep).split(b' ')
209 209 else:
210 210 break
211 211
212 212 # git rev-tree is a confusing thing. You can supply a number of
213 213 # commit sha1s on the command line, and it walks the commit history
214 214 # telling you which commits are reachable from the supplied ones via
215 215 # a bitmask based on arg position.
216 216 # you can specify a commit to stop at by starting the sha1 with ^
217 217 def revtree(ui, args, repo, full="tree", maxnr=0, parents=False):
218 218 def chlogwalk():
219 219 count = len(repo)
220 220 i = count
221 221 l = [0] * 100
222 222 chunk = 100
223 223 while True:
224 224 if chunk > i:
225 225 chunk = i
226 226 i = 0
227 227 else:
228 228 i -= chunk
229 229
230 230 for x in pycompat.xrange(chunk):
231 231 if i + x >= count:
232 232 l[chunk - x:] = [0] * (chunk - x)
233 233 break
234 234 if full is not None:
235 235 if (i + x) in repo:
236 236 l[x] = repo[i + x]
237 237 l[x].changeset() # force reading
238 238 else:
239 239 if (i + x) in repo:
240 240 l[x] = 1
241 241 for x in pycompat.xrange(chunk - 1, -1, -1):
242 242 if l[x] != 0:
243 243 yield (i + x, full is not None and l[x] or None)
244 244 if i == 0:
245 245 break
246 246
247 247 # calculate and return the reachability bitmask for sha
248 248 def is_reachable(ar, reachable, sha):
249 249 if len(ar) == 0:
250 250 return 1
251 251 mask = 0
252 252 for i in pycompat.xrange(len(ar)):
253 253 if sha in reachable[i]:
254 254 mask |= 1 << i
255 255
256 256 return mask
257 257
258 258 reachable = []
259 259 stop_sha1 = []
260 260 want_sha1 = []
261 261 count = 0
262 262
263 263 # figure out which commits they are asking for and which ones they
264 264 # want us to stop on
265 265 for i, arg in enumerate(args):
266 266 if arg.startswith('^'):
267 267 s = repo.lookup(arg[1:])
268 268 stop_sha1.append(s)
269 269 want_sha1.append(s)
270 270 elif arg != 'HEAD':
271 271 want_sha1.append(repo.lookup(arg))
272 272
273 273 # calculate the graph for the supplied commits
274 274 for i, n in enumerate(want_sha1):
275 275 reachable.append(set())
276 276 visit = [n]
277 277 reachable[i].add(n)
278 278 while visit:
279 279 n = visit.pop(0)
280 280 if n in stop_sha1:
281 281 continue
282 282 for p in repo.changelog.parents(n):
283 283 if p not in reachable[i]:
284 284 reachable[i].add(p)
285 285 visit.append(p)
286 286 if p in stop_sha1:
287 287 continue
288 288
289 289 # walk the repository looking for commits that are in our
290 290 # reachability graph
291 291 for i, ctx in chlogwalk():
292 292 if i not in repo:
293 293 continue
294 294 n = repo.changelog.node(i)
295 295 mask = is_reachable(want_sha1, reachable, n)
296 296 if mask:
297 297 parentstr = ""
298 298 if parents:
299 299 pp = repo.changelog.parents(n)
300 300 if pp[0] != nullid:
301 301 parentstr += " " + short(pp[0])
302 302 if pp[1] != nullid:
303 303 parentstr += " " + short(pp[1])
304 304 if not full:
305 305 ui.write("%s%s\n" % (short(n), parentstr))
306 306 elif full == "commit":
307 307 ui.write("%s%s\n" % (short(n), parentstr))
308 308 catcommit(ui, repo, n, ' ', ctx)
309 309 else:
310 310 (p1, p2) = repo.changelog.parents(n)
311 311 (h, h1, h2) = map(short, (n, p1, p2))
312 312 (i1, i2) = map(repo.changelog.rev, (p1, p2))
313 313
314 314 date = ctx.date()[0]
315 315 ui.write("%s %s:%s" % (date, h, mask))
316 316 mask = is_reachable(want_sha1, reachable, p1)
317 317 if i1 != nullrev and mask > 0:
318 318 ui.write("%s:%s " % (h1, mask)),
319 319 mask = is_reachable(want_sha1, reachable, p2)
320 320 if i2 != nullrev and mask > 0:
321 321 ui.write("%s:%s " % (h2, mask))
322 322 ui.write("\n")
323 323 if maxnr and count >= maxnr:
324 324 break
325 325 count += 1
326 326
327 327 # git rev-list tries to order things by date, and has the ability to stop
328 328 # at a given commit without walking the whole repo. TODO add the stop
329 329 # parameter
330 330 @command('debug-rev-list',
331 331 [('H', 'header', None, _('header')),
332 332 ('t', 'topo-order', None, _('topo-order')),
333 333 ('p', 'parents', None, _('parents')),
334 334 ('n', 'max-count', 0, _('max-count'))],
335 335 ('[OPTION]... REV...'))
336 336 def revlist(ui, repo, *revs, **opts):
337 337 """print revisions"""
338 338 if opts['header']:
339 339 full = "commit"
340 340 else:
341 341 full = None
342 342 copy = [x for x in revs]
343 343 revtree(ui, copy, repo, full, opts[r'max_count'], opts[r'parents'])
344 344
345 345 @command('view',
346 346 [('l', 'limit', '',
347 347 _('limit number of changes displayed'), _('NUM'))],
348 _('[-l LIMIT] [REVRANGE]'))
348 _('[-l LIMIT] [REVRANGE]'),
349 helpcategory=command.CATEGORY_CHANGE_NAVIGATION)
349 350 def view(ui, repo, *etc, **opts):
350 351 "start interactive history viewer"
351 352 opts = pycompat.byteskwargs(opts)
352 353 os.chdir(repo.root)
353 354 optstr = ' '.join(['--%s %s' % (k, v) for k, v in opts.iteritems() if v])
354 355 if repo.filtername is None:
355 356 optstr += '--hidden'
356 357
357 358 cmd = ui.config("hgk", "path") + " %s %s" % (optstr, " ".join(etc))
358 359 ui.debug("running %s\n" % cmd)
359 360 ui.system(cmd, blockedtag='hgk_view')
@@ -1,1658 +1,1659 b''
1 1 # histedit.py - interactive history editing for mercurial
2 2 #
3 3 # Copyright 2009 Augie Fackler <raf@durin42.com>
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 """interactive history editing
8 8
9 9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 10 is as follows, assuming the following history::
11 11
12 12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 13 | Add delta
14 14 |
15 15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 16 | Add gamma
17 17 |
18 18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 19 | Add beta
20 20 |
21 21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 22 Add alpha
23 23
24 24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 25 file open in your editor::
26 26
27 27 pick c561b4e977df Add beta
28 28 pick 030b686bedc4 Add gamma
29 29 pick 7c2fd3b9020c Add delta
30 30
31 31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 32 #
33 33 # Commits are listed from least to most recent
34 34 #
35 35 # Commands:
36 36 # p, pick = use commit
37 37 # e, edit = use commit, but stop for amending
38 38 # f, fold = use commit, but combine it with the one above
39 39 # r, roll = like fold, but discard this commit's description and date
40 40 # d, drop = remove commit from history
41 41 # m, mess = edit commit message without changing commit content
42 42 # b, base = checkout changeset and apply further changesets from there
43 43 #
44 44
45 45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
46 46 for each revision in your history. For example, if you had meant to add gamma
47 47 before beta, and then wanted to add delta in the same revision as beta, you
48 48 would reorganize the file to look like this::
49 49
50 50 pick 030b686bedc4 Add gamma
51 51 pick c561b4e977df Add beta
52 52 fold 7c2fd3b9020c Add delta
53 53
54 54 # Edit history between c561b4e977df and 7c2fd3b9020c
55 55 #
56 56 # Commits are listed from least to most recent
57 57 #
58 58 # Commands:
59 59 # p, pick = use commit
60 60 # e, edit = use commit, but stop for amending
61 61 # f, fold = use commit, but combine it with the one above
62 62 # r, roll = like fold, but discard this commit's description and date
63 63 # d, drop = remove commit from history
64 64 # m, mess = edit commit message without changing commit content
65 65 # b, base = checkout changeset and apply further changesets from there
66 66 #
67 67
68 68 At which point you close the editor and ``histedit`` starts working. When you
69 69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
70 70 those revisions together, offering you a chance to clean up the commit message::
71 71
72 72 Add beta
73 73 ***
74 74 Add delta
75 75
76 76 Edit the commit message to your liking, then close the editor. The date used
77 77 for the commit will be the later of the two commits' dates. For this example,
78 78 let's assume that the commit message was changed to ``Add beta and delta.``
79 79 After histedit has run and had a chance to remove any old or temporary
80 80 revisions it needed, the history looks like this::
81 81
82 82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
83 83 | Add beta and delta.
84 84 |
85 85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
86 86 | Add gamma
87 87 |
88 88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
89 89 Add alpha
90 90
91 91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
92 92 ones) until after it has completed all the editing operations, so it will
93 93 probably perform several strip operations when it's done. For the above example,
94 94 it had to run strip twice. Strip can be slow depending on a variety of factors,
95 95 so you might need to be a little patient. You can choose to keep the original
96 96 revisions by passing the ``--keep`` flag.
97 97
98 98 The ``edit`` operation will drop you back to a command prompt,
99 99 allowing you to edit files freely, or even use ``hg record`` to commit
100 100 some changes as a separate commit. When you're done, any remaining
101 101 uncommitted changes will be committed as well. When done, run ``hg
102 102 histedit --continue`` to finish this step. If there are uncommitted
103 103 changes, you'll be prompted for a new commit message, but the default
104 104 commit message will be the original message for the ``edit`` ed
105 105 revision, and the date of the original commit will be preserved.
106 106
107 107 The ``message`` operation will give you a chance to revise a commit
108 108 message without changing the contents. It's a shortcut for doing
109 109 ``edit`` immediately followed by `hg histedit --continue``.
110 110
111 111 If ``histedit`` encounters a conflict when moving a revision (while
112 112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
113 113 ``edit`` with the difference that it won't prompt you for a commit
114 114 message when done. If you decide at this point that you don't like how
115 115 much work it will be to rearrange history, or that you made a mistake,
116 116 you can use ``hg histedit --abort`` to abandon the new changes you
117 117 have made and return to the state before you attempted to edit your
118 118 history.
119 119
120 120 If we clone the histedit-ed example repository above and add four more
121 121 changes, such that we have the following history::
122 122
123 123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
124 124 | Add theta
125 125 |
126 126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
127 127 | Add eta
128 128 |
129 129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
130 130 | Add zeta
131 131 |
132 132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
133 133 | Add epsilon
134 134 |
135 135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
136 136 | Add beta and delta.
137 137 |
138 138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
139 139 | Add gamma
140 140 |
141 141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
142 142 Add alpha
143 143
144 144 If you run ``hg histedit --outgoing`` on the clone then it is the same
145 145 as running ``hg histedit 836302820282``. If you need plan to push to a
146 146 repository that Mercurial does not detect to be related to the source
147 147 repo, you can add a ``--force`` option.
148 148
149 149 Config
150 150 ------
151 151
152 152 Histedit rule lines are truncated to 80 characters by default. You
153 153 can customize this behavior by setting a different length in your
154 154 configuration file::
155 155
156 156 [histedit]
157 157 linelen = 120 # truncate rule lines at 120 characters
158 158
159 159 ``hg histedit`` attempts to automatically choose an appropriate base
160 160 revision to use. To change which base revision is used, define a
161 161 revset in your configuration file::
162 162
163 163 [histedit]
164 164 defaultrev = only(.) & draft()
165 165
166 166 By default each edited revision needs to be present in histedit commands.
167 167 To remove revision you need to use ``drop`` operation. You can configure
168 168 the drop to be implicit for missing commits by adding::
169 169
170 170 [histedit]
171 171 dropmissing = True
172 172
173 173 By default, histedit will close the transaction after each action. For
174 174 performance purposes, you can configure histedit to use a single transaction
175 175 across the entire histedit. WARNING: This setting introduces a significant risk
176 176 of losing the work you've done in a histedit if the histedit aborts
177 177 unexpectedly::
178 178
179 179 [histedit]
180 180 singletransaction = True
181 181
182 182 """
183 183
184 184 from __future__ import absolute_import
185 185
186 186 import os
187 187
188 188 from mercurial.i18n import _
189 189 from mercurial import (
190 190 bundle2,
191 191 cmdutil,
192 192 context,
193 193 copies,
194 194 destutil,
195 195 discovery,
196 196 error,
197 197 exchange,
198 198 extensions,
199 199 hg,
200 200 lock,
201 201 merge as mergemod,
202 202 mergeutil,
203 203 node,
204 204 obsolete,
205 205 pycompat,
206 206 registrar,
207 207 repair,
208 208 scmutil,
209 209 state as statemod,
210 210 util,
211 211 )
212 212 from mercurial.utils import (
213 213 stringutil,
214 214 )
215 215
216 216 pickle = util.pickle
217 217 release = lock.release
218 218 cmdtable = {}
219 219 command = registrar.command(cmdtable)
220 220
221 221 configtable = {}
222 222 configitem = registrar.configitem(configtable)
223 223 configitem('experimental', 'histedit.autoverb',
224 224 default=False,
225 225 )
226 226 configitem('histedit', 'defaultrev',
227 227 default=None,
228 228 )
229 229 configitem('histedit', 'dropmissing',
230 230 default=False,
231 231 )
232 232 configitem('histedit', 'linelen',
233 233 default=80,
234 234 )
235 235 configitem('histedit', 'singletransaction',
236 236 default=False,
237 237 )
238 238
239 239 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
240 240 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
241 241 # be specifying the version(s) of Mercurial they are tested with, or
242 242 # leave the attribute unspecified.
243 243 testedwith = 'ships-with-hg-core'
244 244
245 245 actiontable = {}
246 246 primaryactions = set()
247 247 secondaryactions = set()
248 248 tertiaryactions = set()
249 249 internalactions = set()
250 250
251 251 def geteditcomment(ui, first, last):
252 252 """ construct the editor comment
253 253 The comment includes::
254 254 - an intro
255 255 - sorted primary commands
256 256 - sorted short commands
257 257 - sorted long commands
258 258 - additional hints
259 259
260 260 Commands are only included once.
261 261 """
262 262 intro = _("""Edit history between %s and %s
263 263
264 264 Commits are listed from least to most recent
265 265
266 266 You can reorder changesets by reordering the lines
267 267
268 268 Commands:
269 269 """)
270 270 actions = []
271 271 def addverb(v):
272 272 a = actiontable[v]
273 273 lines = a.message.split("\n")
274 274 if len(a.verbs):
275 275 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
276 276 actions.append(" %s = %s" % (v, lines[0]))
277 277 actions.extend([' %s' for l in lines[1:]])
278 278
279 279 for v in (
280 280 sorted(primaryactions) +
281 281 sorted(secondaryactions) +
282 282 sorted(tertiaryactions)
283 283 ):
284 284 addverb(v)
285 285 actions.append('')
286 286
287 287 hints = []
288 288 if ui.configbool('histedit', 'dropmissing'):
289 289 hints.append("Deleting a changeset from the list "
290 290 "will DISCARD it from the edited history!")
291 291
292 292 lines = (intro % (first, last)).split('\n') + actions + hints
293 293
294 294 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
295 295
296 296 class histeditstate(object):
297 297 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
298 298 topmost=None, replacements=None, lock=None, wlock=None):
299 299 self.repo = repo
300 300 self.actions = actions
301 301 self.keep = keep
302 302 self.topmost = topmost
303 303 self.parentctxnode = parentctxnode
304 304 self.lock = lock
305 305 self.wlock = wlock
306 306 self.backupfile = None
307 307 self.stateobj = statemod.cmdstate(repo, 'histedit-state')
308 308 if replacements is None:
309 309 self.replacements = []
310 310 else:
311 311 self.replacements = replacements
312 312
313 313 def read(self):
314 314 """Load histedit state from disk and set fields appropriately."""
315 315 if not self.stateobj.exists():
316 316 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
317 317
318 318 data = self._read()
319 319
320 320 self.parentctxnode = data['parentctxnode']
321 321 actions = parserules(data['rules'], self)
322 322 self.actions = actions
323 323 self.keep = data['keep']
324 324 self.topmost = data['topmost']
325 325 self.replacements = data['replacements']
326 326 self.backupfile = data['backupfile']
327 327
328 328 def _read(self):
329 329 fp = self.repo.vfs.read('histedit-state')
330 330 if fp.startswith('v1\n'):
331 331 data = self._load()
332 332 parentctxnode, rules, keep, topmost, replacements, backupfile = data
333 333 else:
334 334 data = pickle.loads(fp)
335 335 parentctxnode, rules, keep, topmost, replacements = data
336 336 backupfile = None
337 337 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
338 338
339 339 return {'parentctxnode': parentctxnode, "rules": rules, "keep": keep,
340 340 "topmost": topmost, "replacements": replacements,
341 341 "backupfile": backupfile}
342 342
343 343 def write(self, tr=None):
344 344 if tr:
345 345 tr.addfilegenerator('histedit-state', ('histedit-state',),
346 346 self._write, location='plain')
347 347 else:
348 348 with self.repo.vfs("histedit-state", "w") as f:
349 349 self._write(f)
350 350
351 351 def _write(self, fp):
352 352 fp.write('v1\n')
353 353 fp.write('%s\n' % node.hex(self.parentctxnode))
354 354 fp.write('%s\n' % node.hex(self.topmost))
355 355 fp.write('%s\n' % ('True' if self.keep else 'False'))
356 356 fp.write('%d\n' % len(self.actions))
357 357 for action in self.actions:
358 358 fp.write('%s\n' % action.tostate())
359 359 fp.write('%d\n' % len(self.replacements))
360 360 for replacement in self.replacements:
361 361 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
362 362 for r in replacement[1])))
363 363 backupfile = self.backupfile
364 364 if not backupfile:
365 365 backupfile = ''
366 366 fp.write('%s\n' % backupfile)
367 367
368 368 def _load(self):
369 369 fp = self.repo.vfs('histedit-state', 'r')
370 370 lines = [l[:-1] for l in fp.readlines()]
371 371
372 372 index = 0
373 373 lines[index] # version number
374 374 index += 1
375 375
376 376 parentctxnode = node.bin(lines[index])
377 377 index += 1
378 378
379 379 topmost = node.bin(lines[index])
380 380 index += 1
381 381
382 382 keep = lines[index] == 'True'
383 383 index += 1
384 384
385 385 # Rules
386 386 rules = []
387 387 rulelen = int(lines[index])
388 388 index += 1
389 389 for i in pycompat.xrange(rulelen):
390 390 ruleaction = lines[index]
391 391 index += 1
392 392 rule = lines[index]
393 393 index += 1
394 394 rules.append((ruleaction, rule))
395 395
396 396 # Replacements
397 397 replacements = []
398 398 replacementlen = int(lines[index])
399 399 index += 1
400 400 for i in pycompat.xrange(replacementlen):
401 401 replacement = lines[index]
402 402 original = node.bin(replacement[:40])
403 403 succ = [node.bin(replacement[i:i + 40]) for i in
404 404 range(40, len(replacement), 40)]
405 405 replacements.append((original, succ))
406 406 index += 1
407 407
408 408 backupfile = lines[index]
409 409 index += 1
410 410
411 411 fp.close()
412 412
413 413 return parentctxnode, rules, keep, topmost, replacements, backupfile
414 414
415 415 def clear(self):
416 416 if self.inprogress():
417 417 self.repo.vfs.unlink('histedit-state')
418 418
419 419 def inprogress(self):
420 420 return self.repo.vfs.exists('histedit-state')
421 421
422 422
423 423 class histeditaction(object):
424 424 def __init__(self, state, node):
425 425 self.state = state
426 426 self.repo = state.repo
427 427 self.node = node
428 428
429 429 @classmethod
430 430 def fromrule(cls, state, rule):
431 431 """Parses the given rule, returning an instance of the histeditaction.
432 432 """
433 433 ruleid = rule.strip().split(' ', 1)[0]
434 434 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
435 435 # Check for validation of rule ids and get the rulehash
436 436 try:
437 437 rev = node.bin(ruleid)
438 438 except TypeError:
439 439 try:
440 440 _ctx = scmutil.revsingle(state.repo, ruleid)
441 441 rulehash = _ctx.hex()
442 442 rev = node.bin(rulehash)
443 443 except error.RepoLookupError:
444 444 raise error.ParseError(_("invalid changeset %s") % ruleid)
445 445 return cls(state, rev)
446 446
447 447 def verify(self, prev, expected, seen):
448 448 """ Verifies semantic correctness of the rule"""
449 449 repo = self.repo
450 450 ha = node.hex(self.node)
451 451 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
452 452 if self.node is None:
453 453 raise error.ParseError(_('unknown changeset %s listed') % ha[:12])
454 454 self._verifynodeconstraints(prev, expected, seen)
455 455
456 456 def _verifynodeconstraints(self, prev, expected, seen):
457 457 # by default command need a node in the edited list
458 458 if self.node not in expected:
459 459 raise error.ParseError(_('%s "%s" changeset was not a candidate')
460 460 % (self.verb, node.short(self.node)),
461 461 hint=_('only use listed changesets'))
462 462 # and only one command per node
463 463 if self.node in seen:
464 464 raise error.ParseError(_('duplicated command for changeset %s') %
465 465 node.short(self.node))
466 466
467 467 def torule(self):
468 468 """build a histedit rule line for an action
469 469
470 470 by default lines are in the form:
471 471 <hash> <rev> <summary>
472 472 """
473 473 ctx = self.repo[self.node]
474 474 summary = _getsummary(ctx)
475 475 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
476 476 # trim to 75 columns by default so it's not stupidly wide in my editor
477 477 # (the 5 more are left for verb)
478 478 maxlen = self.repo.ui.configint('histedit', 'linelen')
479 479 maxlen = max(maxlen, 22) # avoid truncating hash
480 480 return stringutil.ellipsis(line, maxlen)
481 481
482 482 def tostate(self):
483 483 """Print an action in format used by histedit state files
484 484 (the first line is a verb, the remainder is the second)
485 485 """
486 486 return "%s\n%s" % (self.verb, node.hex(self.node))
487 487
488 488 def run(self):
489 489 """Runs the action. The default behavior is simply apply the action's
490 490 rulectx onto the current parentctx."""
491 491 self.applychange()
492 492 self.continuedirty()
493 493 return self.continueclean()
494 494
495 495 def applychange(self):
496 496 """Applies the changes from this action's rulectx onto the current
497 497 parentctx, but does not commit them."""
498 498 repo = self.repo
499 499 rulectx = repo[self.node]
500 500 repo.ui.pushbuffer(error=True, labeled=True)
501 501 hg.update(repo, self.state.parentctxnode, quietempty=True)
502 502 stats = applychanges(repo.ui, repo, rulectx, {})
503 503 repo.dirstate.setbranch(rulectx.branch())
504 504 if stats.unresolvedcount:
505 505 buf = repo.ui.popbuffer()
506 506 repo.ui.write(buf)
507 507 raise error.InterventionRequired(
508 508 _('Fix up the change (%s %s)') %
509 509 (self.verb, node.short(self.node)),
510 510 hint=_('hg histedit --continue to resume'))
511 511 else:
512 512 repo.ui.popbuffer()
513 513
514 514 def continuedirty(self):
515 515 """Continues the action when changes have been applied to the working
516 516 copy. The default behavior is to commit the dirty changes."""
517 517 repo = self.repo
518 518 rulectx = repo[self.node]
519 519
520 520 editor = self.commiteditor()
521 521 commit = commitfuncfor(repo, rulectx)
522 522
523 523 commit(text=rulectx.description(), user=rulectx.user(),
524 524 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
525 525
526 526 def commiteditor(self):
527 527 """The editor to be used to edit the commit message."""
528 528 return False
529 529
530 530 def continueclean(self):
531 531 """Continues the action when the working copy is clean. The default
532 532 behavior is to accept the current commit as the new version of the
533 533 rulectx."""
534 534 ctx = self.repo['.']
535 535 if ctx.node() == self.state.parentctxnode:
536 536 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
537 537 node.short(self.node))
538 538 return ctx, [(self.node, tuple())]
539 539 if ctx.node() == self.node:
540 540 # Nothing changed
541 541 return ctx, []
542 542 return ctx, [(self.node, (ctx.node(),))]
543 543
544 544 def commitfuncfor(repo, src):
545 545 """Build a commit function for the replacement of <src>
546 546
547 547 This function ensure we apply the same treatment to all changesets.
548 548
549 549 - Add a 'histedit_source' entry in extra.
550 550
551 551 Note that fold has its own separated logic because its handling is a bit
552 552 different and not easily factored out of the fold method.
553 553 """
554 554 phasemin = src.phase()
555 555 def commitfunc(**kwargs):
556 556 overrides = {('phases', 'new-commit'): phasemin}
557 557 with repo.ui.configoverride(overrides, 'histedit'):
558 558 extra = kwargs.get(r'extra', {}).copy()
559 559 extra['histedit_source'] = src.hex()
560 560 kwargs[r'extra'] = extra
561 561 return repo.commit(**kwargs)
562 562 return commitfunc
563 563
564 564 def applychanges(ui, repo, ctx, opts):
565 565 """Merge changeset from ctx (only) in the current working directory"""
566 566 wcpar = repo.dirstate.parents()[0]
567 567 if ctx.p1().node() == wcpar:
568 568 # edits are "in place" we do not need to make any merge,
569 569 # just applies changes on parent for editing
570 570 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
571 571 stats = mergemod.updateresult(0, 0, 0, 0)
572 572 else:
573 573 try:
574 574 # ui.forcemerge is an internal variable, do not document
575 575 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
576 576 'histedit')
577 577 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
578 578 finally:
579 579 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
580 580 return stats
581 581
582 582 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
583 583 """collapse the set of revisions from first to last as new one.
584 584
585 585 Expected commit options are:
586 586 - message
587 587 - date
588 588 - username
589 589 Commit message is edited in all cases.
590 590
591 591 This function works in memory."""
592 592 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
593 593 if not ctxs:
594 594 return None
595 595 for c in ctxs:
596 596 if not c.mutable():
597 597 raise error.ParseError(
598 598 _("cannot fold into public change %s") % node.short(c.node()))
599 599 base = firstctx.parents()[0]
600 600
601 601 # commit a new version of the old changeset, including the update
602 602 # collect all files which might be affected
603 603 files = set()
604 604 for ctx in ctxs:
605 605 files.update(ctx.files())
606 606
607 607 # Recompute copies (avoid recording a -> b -> a)
608 608 copied = copies.pathcopies(base, lastctx)
609 609
610 610 # prune files which were reverted by the updates
611 611 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
612 612 # commit version of these files as defined by head
613 613 headmf = lastctx.manifest()
614 614 def filectxfn(repo, ctx, path):
615 615 if path in headmf:
616 616 fctx = lastctx[path]
617 617 flags = fctx.flags()
618 618 mctx = context.memfilectx(repo, ctx,
619 619 fctx.path(), fctx.data(),
620 620 islink='l' in flags,
621 621 isexec='x' in flags,
622 622 copied=copied.get(path))
623 623 return mctx
624 624 return None
625 625
626 626 if commitopts.get('message'):
627 627 message = commitopts['message']
628 628 else:
629 629 message = firstctx.description()
630 630 user = commitopts.get('user')
631 631 date = commitopts.get('date')
632 632 extra = commitopts.get('extra')
633 633
634 634 parents = (firstctx.p1().node(), firstctx.p2().node())
635 635 editor = None
636 636 if not skipprompt:
637 637 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
638 638 new = context.memctx(repo,
639 639 parents=parents,
640 640 text=message,
641 641 files=files,
642 642 filectxfn=filectxfn,
643 643 user=user,
644 644 date=date,
645 645 extra=extra,
646 646 editor=editor)
647 647 return repo.commitctx(new)
648 648
649 649 def _isdirtywc(repo):
650 650 return repo[None].dirty(missing=True)
651 651
652 652 def abortdirty():
653 653 raise error.Abort(_('working copy has pending changes'),
654 654 hint=_('amend, commit, or revert them and run histedit '
655 655 '--continue, or abort with histedit --abort'))
656 656
657 657 def action(verbs, message, priority=False, internal=False):
658 658 def wrap(cls):
659 659 assert not priority or not internal
660 660 verb = verbs[0]
661 661 if priority:
662 662 primaryactions.add(verb)
663 663 elif internal:
664 664 internalactions.add(verb)
665 665 elif len(verbs) > 1:
666 666 secondaryactions.add(verb)
667 667 else:
668 668 tertiaryactions.add(verb)
669 669
670 670 cls.verb = verb
671 671 cls.verbs = verbs
672 672 cls.message = message
673 673 for verb in verbs:
674 674 actiontable[verb] = cls
675 675 return cls
676 676 return wrap
677 677
678 678 @action(['pick', 'p'],
679 679 _('use commit'),
680 680 priority=True)
681 681 class pick(histeditaction):
682 682 def run(self):
683 683 rulectx = self.repo[self.node]
684 684 if rulectx.parents()[0].node() == self.state.parentctxnode:
685 685 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
686 686 return rulectx, []
687 687
688 688 return super(pick, self).run()
689 689
690 690 @action(['edit', 'e'],
691 691 _('use commit, but stop for amending'),
692 692 priority=True)
693 693 class edit(histeditaction):
694 694 def run(self):
695 695 repo = self.repo
696 696 rulectx = repo[self.node]
697 697 hg.update(repo, self.state.parentctxnode, quietempty=True)
698 698 applychanges(repo.ui, repo, rulectx, {})
699 699 raise error.InterventionRequired(
700 700 _('Editing (%s), you may commit or record as needed now.')
701 701 % node.short(self.node),
702 702 hint=_('hg histedit --continue to resume'))
703 703
704 704 def commiteditor(self):
705 705 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
706 706
707 707 @action(['fold', 'f'],
708 708 _('use commit, but combine it with the one above'))
709 709 class fold(histeditaction):
710 710 def verify(self, prev, expected, seen):
711 711 """ Verifies semantic correctness of the fold rule"""
712 712 super(fold, self).verify(prev, expected, seen)
713 713 repo = self.repo
714 714 if not prev:
715 715 c = repo[self.node].parents()[0]
716 716 elif not prev.verb in ('pick', 'base'):
717 717 return
718 718 else:
719 719 c = repo[prev.node]
720 720 if not c.mutable():
721 721 raise error.ParseError(
722 722 _("cannot fold into public change %s") % node.short(c.node()))
723 723
724 724
725 725 def continuedirty(self):
726 726 repo = self.repo
727 727 rulectx = repo[self.node]
728 728
729 729 commit = commitfuncfor(repo, rulectx)
730 730 commit(text='fold-temp-revision %s' % node.short(self.node),
731 731 user=rulectx.user(), date=rulectx.date(),
732 732 extra=rulectx.extra())
733 733
734 734 def continueclean(self):
735 735 repo = self.repo
736 736 ctx = repo['.']
737 737 rulectx = repo[self.node]
738 738 parentctxnode = self.state.parentctxnode
739 739 if ctx.node() == parentctxnode:
740 740 repo.ui.warn(_('%s: empty changeset\n') %
741 741 node.short(self.node))
742 742 return ctx, [(self.node, (parentctxnode,))]
743 743
744 744 parentctx = repo[parentctxnode]
745 745 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
746 746 parentctx.rev(),
747 747 parentctx.rev()))
748 748 if not newcommits:
749 749 repo.ui.warn(_('%s: cannot fold - working copy is not a '
750 750 'descendant of previous commit %s\n') %
751 751 (node.short(self.node), node.short(parentctxnode)))
752 752 return ctx, [(self.node, (ctx.node(),))]
753 753
754 754 middlecommits = newcommits.copy()
755 755 middlecommits.discard(ctx.node())
756 756
757 757 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
758 758 middlecommits)
759 759
760 760 def skipprompt(self):
761 761 """Returns true if the rule should skip the message editor.
762 762
763 763 For example, 'fold' wants to show an editor, but 'rollup'
764 764 doesn't want to.
765 765 """
766 766 return False
767 767
768 768 def mergedescs(self):
769 769 """Returns true if the rule should merge messages of multiple changes.
770 770
771 771 This exists mainly so that 'rollup' rules can be a subclass of
772 772 'fold'.
773 773 """
774 774 return True
775 775
776 776 def firstdate(self):
777 777 """Returns true if the rule should preserve the date of the first
778 778 change.
779 779
780 780 This exists mainly so that 'rollup' rules can be a subclass of
781 781 'fold'.
782 782 """
783 783 return False
784 784
785 785 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
786 786 parent = ctx.parents()[0].node()
787 787 hg.updaterepo(repo, parent, overwrite=False)
788 788 ### prepare new commit data
789 789 commitopts = {}
790 790 commitopts['user'] = ctx.user()
791 791 # commit message
792 792 if not self.mergedescs():
793 793 newmessage = ctx.description()
794 794 else:
795 795 newmessage = '\n***\n'.join(
796 796 [ctx.description()] +
797 797 [repo[r].description() for r in internalchanges] +
798 798 [oldctx.description()]) + '\n'
799 799 commitopts['message'] = newmessage
800 800 # date
801 801 if self.firstdate():
802 802 commitopts['date'] = ctx.date()
803 803 else:
804 804 commitopts['date'] = max(ctx.date(), oldctx.date())
805 805 extra = ctx.extra().copy()
806 806 # histedit_source
807 807 # note: ctx is likely a temporary commit but that the best we can do
808 808 # here. This is sufficient to solve issue3681 anyway.
809 809 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
810 810 commitopts['extra'] = extra
811 811 phasemin = max(ctx.phase(), oldctx.phase())
812 812 overrides = {('phases', 'new-commit'): phasemin}
813 813 with repo.ui.configoverride(overrides, 'histedit'):
814 814 n = collapse(repo, ctx, repo[newnode], commitopts,
815 815 skipprompt=self.skipprompt())
816 816 if n is None:
817 817 return ctx, []
818 818 hg.updaterepo(repo, n, overwrite=False)
819 819 replacements = [(oldctx.node(), (newnode,)),
820 820 (ctx.node(), (n,)),
821 821 (newnode, (n,)),
822 822 ]
823 823 for ich in internalchanges:
824 824 replacements.append((ich, (n,)))
825 825 return repo[n], replacements
826 826
827 827 @action(['base', 'b'],
828 828 _('checkout changeset and apply further changesets from there'))
829 829 class base(histeditaction):
830 830
831 831 def run(self):
832 832 if self.repo['.'].node() != self.node:
833 833 mergemod.update(self.repo, self.node, False, True)
834 834 # branchmerge, force)
835 835 return self.continueclean()
836 836
837 837 def continuedirty(self):
838 838 abortdirty()
839 839
840 840 def continueclean(self):
841 841 basectx = self.repo['.']
842 842 return basectx, []
843 843
844 844 def _verifynodeconstraints(self, prev, expected, seen):
845 845 # base can only be use with a node not in the edited set
846 846 if self.node in expected:
847 847 msg = _('%s "%s" changeset was an edited list candidate')
848 848 raise error.ParseError(
849 849 msg % (self.verb, node.short(self.node)),
850 850 hint=_('base must only use unlisted changesets'))
851 851
852 852 @action(['_multifold'],
853 853 _(
854 854 """fold subclass used for when multiple folds happen in a row
855 855
856 856 We only want to fire the editor for the folded message once when
857 857 (say) four changes are folded down into a single change. This is
858 858 similar to rollup, but we should preserve both messages so that
859 859 when the last fold operation runs we can show the user all the
860 860 commit messages in their editor.
861 861 """),
862 862 internal=True)
863 863 class _multifold(fold):
864 864 def skipprompt(self):
865 865 return True
866 866
867 867 @action(["roll", "r"],
868 868 _("like fold, but discard this commit's description and date"))
869 869 class rollup(fold):
870 870 def mergedescs(self):
871 871 return False
872 872
873 873 def skipprompt(self):
874 874 return True
875 875
876 876 def firstdate(self):
877 877 return True
878 878
879 879 @action(["drop", "d"],
880 880 _('remove commit from history'))
881 881 class drop(histeditaction):
882 882 def run(self):
883 883 parentctx = self.repo[self.state.parentctxnode]
884 884 return parentctx, [(self.node, tuple())]
885 885
886 886 @action(["mess", "m"],
887 887 _('edit commit message without changing commit content'),
888 888 priority=True)
889 889 class message(histeditaction):
890 890 def commiteditor(self):
891 891 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
892 892
893 893 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
894 894 """utility function to find the first outgoing changeset
895 895
896 896 Used by initialization code"""
897 897 if opts is None:
898 898 opts = {}
899 899 dest = ui.expandpath(remote or 'default-push', remote or 'default')
900 900 dest, branches = hg.parseurl(dest, None)[:2]
901 901 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
902 902
903 903 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
904 904 other = hg.peer(repo, opts, dest)
905 905
906 906 if revs:
907 907 revs = [repo.lookup(rev) for rev in revs]
908 908
909 909 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
910 910 if not outgoing.missing:
911 911 raise error.Abort(_('no outgoing ancestors'))
912 912 roots = list(repo.revs("roots(%ln)", outgoing.missing))
913 913 if len(roots) > 1:
914 914 msg = _('there are ambiguous outgoing revisions')
915 915 hint = _("see 'hg help histedit' for more detail")
916 916 raise error.Abort(msg, hint=hint)
917 917 return repo[roots[0]].node()
918 918
919 919 @command('histedit',
920 920 [('', 'commands', '',
921 921 _('read history edits from the specified file'), _('FILE')),
922 922 ('c', 'continue', False, _('continue an edit already in progress')),
923 923 ('', 'edit-plan', False, _('edit remaining actions list')),
924 924 ('k', 'keep', False,
925 925 _("don't strip old nodes after edit is complete")),
926 926 ('', 'abort', False, _('abort an edit in progress')),
927 927 ('o', 'outgoing', False, _('changesets not found in destination')),
928 928 ('f', 'force', False,
929 929 _('force outgoing even for unrelated repositories')),
930 930 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
931 931 cmdutil.formatteropts,
932 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
932 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
933 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
933 934 def histedit(ui, repo, *freeargs, **opts):
934 935 """interactively edit changeset history
935 936
936 937 This command lets you edit a linear series of changesets (up to
937 938 and including the working directory, which should be clean).
938 939 You can:
939 940
940 941 - `pick` to [re]order a changeset
941 942
942 943 - `drop` to omit changeset
943 944
944 945 - `mess` to reword the changeset commit message
945 946
946 947 - `fold` to combine it with the preceding changeset (using the later date)
947 948
948 949 - `roll` like fold, but discarding this commit's description and date
949 950
950 951 - `edit` to edit this changeset (preserving date)
951 952
952 953 - `base` to checkout changeset and apply further changesets from there
953 954
954 955 There are a number of ways to select the root changeset:
955 956
956 957 - Specify ANCESTOR directly
957 958
958 959 - Use --outgoing -- it will be the first linear changeset not
959 960 included in destination. (See :hg:`help config.paths.default-push`)
960 961
961 962 - Otherwise, the value from the "histedit.defaultrev" config option
962 963 is used as a revset to select the base revision when ANCESTOR is not
963 964 specified. The first revision returned by the revset is used. By
964 965 default, this selects the editable history that is unique to the
965 966 ancestry of the working directory.
966 967
967 968 .. container:: verbose
968 969
969 970 If you use --outgoing, this command will abort if there are ambiguous
970 971 outgoing revisions. For example, if there are multiple branches
971 972 containing outgoing revisions.
972 973
973 974 Use "min(outgoing() and ::.)" or similar revset specification
974 975 instead of --outgoing to specify edit target revision exactly in
975 976 such ambiguous situation. See :hg:`help revsets` for detail about
976 977 selecting revisions.
977 978
978 979 .. container:: verbose
979 980
980 981 Examples:
981 982
982 983 - A number of changes have been made.
983 984 Revision 3 is no longer needed.
984 985
985 986 Start history editing from revision 3::
986 987
987 988 hg histedit -r 3
988 989
989 990 An editor opens, containing the list of revisions,
990 991 with specific actions specified::
991 992
992 993 pick 5339bf82f0ca 3 Zworgle the foobar
993 994 pick 8ef592ce7cc4 4 Bedazzle the zerlog
994 995 pick 0a9639fcda9d 5 Morgify the cromulancy
995 996
996 997 Additional information about the possible actions
997 998 to take appears below the list of revisions.
998 999
999 1000 To remove revision 3 from the history,
1000 1001 its action (at the beginning of the relevant line)
1001 1002 is changed to 'drop'::
1002 1003
1003 1004 drop 5339bf82f0ca 3 Zworgle the foobar
1004 1005 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1005 1006 pick 0a9639fcda9d 5 Morgify the cromulancy
1006 1007
1007 1008 - A number of changes have been made.
1008 1009 Revision 2 and 4 need to be swapped.
1009 1010
1010 1011 Start history editing from revision 2::
1011 1012
1012 1013 hg histedit -r 2
1013 1014
1014 1015 An editor opens, containing the list of revisions,
1015 1016 with specific actions specified::
1016 1017
1017 1018 pick 252a1af424ad 2 Blorb a morgwazzle
1018 1019 pick 5339bf82f0ca 3 Zworgle the foobar
1019 1020 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1020 1021
1021 1022 To swap revision 2 and 4, its lines are swapped
1022 1023 in the editor::
1023 1024
1024 1025 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1025 1026 pick 5339bf82f0ca 3 Zworgle the foobar
1026 1027 pick 252a1af424ad 2 Blorb a morgwazzle
1027 1028
1028 1029 Returns 0 on success, 1 if user intervention is required (not only
1029 1030 for intentional "edit" command, but also for resolving unexpected
1030 1031 conflicts).
1031 1032 """
1032 1033 state = histeditstate(repo)
1033 1034 try:
1034 1035 state.wlock = repo.wlock()
1035 1036 state.lock = repo.lock()
1036 1037 _histedit(ui, repo, state, *freeargs, **opts)
1037 1038 finally:
1038 1039 release(state.lock, state.wlock)
1039 1040
1040 1041 goalcontinue = 'continue'
1041 1042 goalabort = 'abort'
1042 1043 goaleditplan = 'edit-plan'
1043 1044 goalnew = 'new'
1044 1045
1045 1046 def _getgoal(opts):
1046 1047 if opts.get('continue'):
1047 1048 return goalcontinue
1048 1049 if opts.get('abort'):
1049 1050 return goalabort
1050 1051 if opts.get('edit_plan'):
1051 1052 return goaleditplan
1052 1053 return goalnew
1053 1054
1054 1055 def _readfile(ui, path):
1055 1056 if path == '-':
1056 1057 with ui.timeblockedsection('histedit'):
1057 1058 return ui.fin.read()
1058 1059 else:
1059 1060 with open(path, 'rb') as f:
1060 1061 return f.read()
1061 1062
1062 1063 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1063 1064 # TODO only abort if we try to histedit mq patches, not just
1064 1065 # blanket if mq patches are applied somewhere
1065 1066 mq = getattr(repo, 'mq', None)
1066 1067 if mq and mq.applied:
1067 1068 raise error.Abort(_('source has mq patches applied'))
1068 1069
1069 1070 # basic argument incompatibility processing
1070 1071 outg = opts.get('outgoing')
1071 1072 editplan = opts.get('edit_plan')
1072 1073 abort = opts.get('abort')
1073 1074 force = opts.get('force')
1074 1075 if force and not outg:
1075 1076 raise error.Abort(_('--force only allowed with --outgoing'))
1076 1077 if goal == 'continue':
1077 1078 if any((outg, abort, revs, freeargs, rules, editplan)):
1078 1079 raise error.Abort(_('no arguments allowed with --continue'))
1079 1080 elif goal == 'abort':
1080 1081 if any((outg, revs, freeargs, rules, editplan)):
1081 1082 raise error.Abort(_('no arguments allowed with --abort'))
1082 1083 elif goal == 'edit-plan':
1083 1084 if any((outg, revs, freeargs)):
1084 1085 raise error.Abort(_('only --commands argument allowed with '
1085 1086 '--edit-plan'))
1086 1087 else:
1087 1088 if state.inprogress():
1088 1089 raise error.Abort(_('history edit already in progress, try '
1089 1090 '--continue or --abort'))
1090 1091 if outg:
1091 1092 if revs:
1092 1093 raise error.Abort(_('no revisions allowed with --outgoing'))
1093 1094 if len(freeargs) > 1:
1094 1095 raise error.Abort(
1095 1096 _('only one repo argument allowed with --outgoing'))
1096 1097 else:
1097 1098 revs.extend(freeargs)
1098 1099 if len(revs) == 0:
1099 1100 defaultrev = destutil.desthistedit(ui, repo)
1100 1101 if defaultrev is not None:
1101 1102 revs.append(defaultrev)
1102 1103
1103 1104 if len(revs) != 1:
1104 1105 raise error.Abort(
1105 1106 _('histedit requires exactly one ancestor revision'))
1106 1107
1107 1108 def _histedit(ui, repo, state, *freeargs, **opts):
1108 1109 opts = pycompat.byteskwargs(opts)
1109 1110 fm = ui.formatter('histedit', opts)
1110 1111 fm.startitem()
1111 1112 goal = _getgoal(opts)
1112 1113 revs = opts.get('rev', [])
1113 1114 # experimental config: ui.history-editing-backup
1114 1115 nobackup = not ui.configbool('ui', 'history-editing-backup')
1115 1116 rules = opts.get('commands', '')
1116 1117 state.keep = opts.get('keep', False)
1117 1118
1118 1119 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1119 1120
1120 1121 # rebuild state
1121 1122 if goal == goalcontinue:
1122 1123 state.read()
1123 1124 state = bootstrapcontinue(ui, state, opts)
1124 1125 elif goal == goaleditplan:
1125 1126 _edithisteditplan(ui, repo, state, rules)
1126 1127 return
1127 1128 elif goal == goalabort:
1128 1129 _aborthistedit(ui, repo, state, nobackup=nobackup)
1129 1130 return
1130 1131 else:
1131 1132 # goal == goalnew
1132 1133 _newhistedit(ui, repo, state, revs, freeargs, opts)
1133 1134
1134 1135 _continuehistedit(ui, repo, state)
1135 1136 _finishhistedit(ui, repo, state, fm)
1136 1137 fm.end()
1137 1138
1138 1139 def _continuehistedit(ui, repo, state):
1139 1140 """This function runs after either:
1140 1141 - bootstrapcontinue (if the goal is 'continue')
1141 1142 - _newhistedit (if the goal is 'new')
1142 1143 """
1143 1144 # preprocess rules so that we can hide inner folds from the user
1144 1145 # and only show one editor
1145 1146 actions = state.actions[:]
1146 1147 for idx, (action, nextact) in enumerate(
1147 1148 zip(actions, actions[1:] + [None])):
1148 1149 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1149 1150 state.actions[idx].__class__ = _multifold
1150 1151
1151 1152 # Force an initial state file write, so the user can run --abort/continue
1152 1153 # even if there's an exception before the first transaction serialize.
1153 1154 state.write()
1154 1155
1155 1156 tr = None
1156 1157 # Don't use singletransaction by default since it rolls the entire
1157 1158 # transaction back if an unexpected exception happens (like a
1158 1159 # pretxncommit hook throws, or the user aborts the commit msg editor).
1159 1160 if ui.configbool("histedit", "singletransaction"):
1160 1161 # Don't use a 'with' for the transaction, since actions may close
1161 1162 # and reopen a transaction. For example, if the action executes an
1162 1163 # external process it may choose to commit the transaction first.
1163 1164 tr = repo.transaction('histedit')
1164 1165 progress = ui.makeprogress(_("editing"), unit=_('changes'),
1165 1166 total=len(state.actions))
1166 1167 with progress, util.acceptintervention(tr):
1167 1168 while state.actions:
1168 1169 state.write(tr=tr)
1169 1170 actobj = state.actions[0]
1170 1171 progress.increment(item=actobj.torule())
1171 1172 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1172 1173 actobj.torule()))
1173 1174 parentctx, replacement_ = actobj.run()
1174 1175 state.parentctxnode = parentctx.node()
1175 1176 state.replacements.extend(replacement_)
1176 1177 state.actions.pop(0)
1177 1178
1178 1179 state.write()
1179 1180
1180 1181 def _finishhistedit(ui, repo, state, fm):
1181 1182 """This action runs when histedit is finishing its session"""
1182 1183 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1183 1184
1184 1185 mapping, tmpnodes, created, ntm = processreplacement(state)
1185 1186 if mapping:
1186 1187 for prec, succs in mapping.iteritems():
1187 1188 if not succs:
1188 1189 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1189 1190 else:
1190 1191 ui.debug('histedit: %s is replaced by %s\n' % (
1191 1192 node.short(prec), node.short(succs[0])))
1192 1193 if len(succs) > 1:
1193 1194 m = 'histedit: %s'
1194 1195 for n in succs[1:]:
1195 1196 ui.debug(m % node.short(n))
1196 1197
1197 1198 if not state.keep:
1198 1199 if mapping:
1199 1200 movetopmostbookmarks(repo, state.topmost, ntm)
1200 1201 # TODO update mq state
1201 1202 else:
1202 1203 mapping = {}
1203 1204
1204 1205 for n in tmpnodes:
1205 1206 if n in repo:
1206 1207 mapping[n] = ()
1207 1208
1208 1209 # remove entries about unknown nodes
1209 1210 nodemap = repo.unfiltered().changelog.nodemap
1210 1211 mapping = {k: v for k, v in mapping.items()
1211 1212 if k in nodemap and all(n in nodemap for n in v)}
1212 1213 scmutil.cleanupnodes(repo, mapping, 'histedit')
1213 1214 hf = fm.hexfunc
1214 1215 fl = fm.formatlist
1215 1216 fd = fm.formatdict
1216 1217 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1217 1218 for oldn, newn in mapping.iteritems()},
1218 1219 key="oldnode", value="newnodes")
1219 1220 fm.data(nodechanges=nodechanges)
1220 1221
1221 1222 state.clear()
1222 1223 if os.path.exists(repo.sjoin('undo')):
1223 1224 os.unlink(repo.sjoin('undo'))
1224 1225 if repo.vfs.exists('histedit-last-edit.txt'):
1225 1226 repo.vfs.unlink('histedit-last-edit.txt')
1226 1227
1227 1228 def _aborthistedit(ui, repo, state, nobackup=False):
1228 1229 try:
1229 1230 state.read()
1230 1231 __, leafs, tmpnodes, __ = processreplacement(state)
1231 1232 ui.debug('restore wc to old parent %s\n'
1232 1233 % node.short(state.topmost))
1233 1234
1234 1235 # Recover our old commits if necessary
1235 1236 if not state.topmost in repo and state.backupfile:
1236 1237 backupfile = repo.vfs.join(state.backupfile)
1237 1238 f = hg.openpath(ui, backupfile)
1238 1239 gen = exchange.readbundle(ui, f, backupfile)
1239 1240 with repo.transaction('histedit.abort') as tr:
1240 1241 bundle2.applybundle(repo, gen, tr, source='histedit',
1241 1242 url='bundle:' + backupfile)
1242 1243
1243 1244 os.remove(backupfile)
1244 1245
1245 1246 # check whether we should update away
1246 1247 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1247 1248 state.parentctxnode, leafs | tmpnodes):
1248 1249 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1249 1250 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1250 1251 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1251 1252 except Exception:
1252 1253 if state.inprogress():
1253 1254 ui.warn(_('warning: encountered an exception during histedit '
1254 1255 '--abort; the repository may not have been completely '
1255 1256 'cleaned up\n'))
1256 1257 raise
1257 1258 finally:
1258 1259 state.clear()
1259 1260
1260 1261 def _edithisteditplan(ui, repo, state, rules):
1261 1262 state.read()
1262 1263 if not rules:
1263 1264 comment = geteditcomment(ui,
1264 1265 node.short(state.parentctxnode),
1265 1266 node.short(state.topmost))
1266 1267 rules = ruleeditor(repo, ui, state.actions, comment)
1267 1268 else:
1268 1269 rules = _readfile(ui, rules)
1269 1270 actions = parserules(rules, state)
1270 1271 ctxs = [repo[act.node] \
1271 1272 for act in state.actions if act.node]
1272 1273 warnverifyactions(ui, repo, actions, state, ctxs)
1273 1274 state.actions = actions
1274 1275 state.write()
1275 1276
1276 1277 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1277 1278 outg = opts.get('outgoing')
1278 1279 rules = opts.get('commands', '')
1279 1280 force = opts.get('force')
1280 1281
1281 1282 cmdutil.checkunfinished(repo)
1282 1283 cmdutil.bailifchanged(repo)
1283 1284
1284 1285 topmost, empty = repo.dirstate.parents()
1285 1286 if outg:
1286 1287 if freeargs:
1287 1288 remote = freeargs[0]
1288 1289 else:
1289 1290 remote = None
1290 1291 root = findoutgoing(ui, repo, remote, force, opts)
1291 1292 else:
1292 1293 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1293 1294 if len(rr) != 1:
1294 1295 raise error.Abort(_('The specified revisions must have '
1295 1296 'exactly one common root'))
1296 1297 root = rr[0].node()
1297 1298
1298 1299 revs = between(repo, root, topmost, state.keep)
1299 1300 if not revs:
1300 1301 raise error.Abort(_('%s is not an ancestor of working directory') %
1301 1302 node.short(root))
1302 1303
1303 1304 ctxs = [repo[r] for r in revs]
1304 1305 if not rules:
1305 1306 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1306 1307 actions = [pick(state, r) for r in revs]
1307 1308 rules = ruleeditor(repo, ui, actions, comment)
1308 1309 else:
1309 1310 rules = _readfile(ui, rules)
1310 1311 actions = parserules(rules, state)
1311 1312 warnverifyactions(ui, repo, actions, state, ctxs)
1312 1313
1313 1314 parentctxnode = repo[root].parents()[0].node()
1314 1315
1315 1316 state.parentctxnode = parentctxnode
1316 1317 state.actions = actions
1317 1318 state.topmost = topmost
1318 1319 state.replacements = []
1319 1320
1320 1321 ui.log("histedit", "%d actions to histedit", len(actions),
1321 1322 histedit_num_actions=len(actions))
1322 1323
1323 1324 # Create a backup so we can always abort completely.
1324 1325 backupfile = None
1325 1326 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1326 1327 backupfile = repair.backupbundle(repo, [parentctxnode],
1327 1328 [topmost], root, 'histedit')
1328 1329 state.backupfile = backupfile
1329 1330
1330 1331 def _getsummary(ctx):
1331 1332 # a common pattern is to extract the summary but default to the empty
1332 1333 # string
1333 1334 summary = ctx.description() or ''
1334 1335 if summary:
1335 1336 summary = summary.splitlines()[0]
1336 1337 return summary
1337 1338
1338 1339 def bootstrapcontinue(ui, state, opts):
1339 1340 repo = state.repo
1340 1341
1341 1342 ms = mergemod.mergestate.read(repo)
1342 1343 mergeutil.checkunresolved(ms)
1343 1344
1344 1345 if state.actions:
1345 1346 actobj = state.actions.pop(0)
1346 1347
1347 1348 if _isdirtywc(repo):
1348 1349 actobj.continuedirty()
1349 1350 if _isdirtywc(repo):
1350 1351 abortdirty()
1351 1352
1352 1353 parentctx, replacements = actobj.continueclean()
1353 1354
1354 1355 state.parentctxnode = parentctx.node()
1355 1356 state.replacements.extend(replacements)
1356 1357
1357 1358 return state
1358 1359
1359 1360 def between(repo, old, new, keep):
1360 1361 """select and validate the set of revision to edit
1361 1362
1362 1363 When keep is false, the specified set can't have children."""
1363 1364 revs = repo.revs('%n::%n', old, new)
1364 1365 if revs and not keep:
1365 1366 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1366 1367 repo.revs('(%ld::) - (%ld)', revs, revs)):
1367 1368 raise error.Abort(_('can only histedit a changeset together '
1368 1369 'with all its descendants'))
1369 1370 if repo.revs('(%ld) and merge()', revs):
1370 1371 raise error.Abort(_('cannot edit history that contains merges'))
1371 1372 root = repo[revs.first()] # list is already sorted by repo.revs()
1372 1373 if not root.mutable():
1373 1374 raise error.Abort(_('cannot edit public changeset: %s') % root,
1374 1375 hint=_("see 'hg help phases' for details"))
1375 1376 return pycompat.maplist(repo.changelog.node, revs)
1376 1377
1377 1378 def ruleeditor(repo, ui, actions, editcomment=""):
1378 1379 """open an editor to edit rules
1379 1380
1380 1381 rules are in the format [ [act, ctx], ...] like in state.rules
1381 1382 """
1382 1383 if repo.ui.configbool("experimental", "histedit.autoverb"):
1383 1384 newact = util.sortdict()
1384 1385 for act in actions:
1385 1386 ctx = repo[act.node]
1386 1387 summary = _getsummary(ctx)
1387 1388 fword = summary.split(' ', 1)[0].lower()
1388 1389 added = False
1389 1390
1390 1391 # if it doesn't end with the special character '!' just skip this
1391 1392 if fword.endswith('!'):
1392 1393 fword = fword[:-1]
1393 1394 if fword in primaryactions | secondaryactions | tertiaryactions:
1394 1395 act.verb = fword
1395 1396 # get the target summary
1396 1397 tsum = summary[len(fword) + 1:].lstrip()
1397 1398 # safe but slow: reverse iterate over the actions so we
1398 1399 # don't clash on two commits having the same summary
1399 1400 for na, l in reversed(list(newact.iteritems())):
1400 1401 actx = repo[na.node]
1401 1402 asum = _getsummary(actx)
1402 1403 if asum == tsum:
1403 1404 added = True
1404 1405 l.append(act)
1405 1406 break
1406 1407
1407 1408 if not added:
1408 1409 newact[act] = []
1409 1410
1410 1411 # copy over and flatten the new list
1411 1412 actions = []
1412 1413 for na, l in newact.iteritems():
1413 1414 actions.append(na)
1414 1415 actions += l
1415 1416
1416 1417 rules = '\n'.join([act.torule() for act in actions])
1417 1418 rules += '\n\n'
1418 1419 rules += editcomment
1419 1420 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1420 1421 repopath=repo.path, action='histedit')
1421 1422
1422 1423 # Save edit rules in .hg/histedit-last-edit.txt in case
1423 1424 # the user needs to ask for help after something
1424 1425 # surprising happens.
1425 1426 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
1426 1427 f.write(rules)
1427 1428
1428 1429 return rules
1429 1430
1430 1431 def parserules(rules, state):
1431 1432 """Read the histedit rules string and return list of action objects """
1432 1433 rules = [l for l in (r.strip() for r in rules.splitlines())
1433 1434 if l and not l.startswith('#')]
1434 1435 actions = []
1435 1436 for r in rules:
1436 1437 if ' ' not in r:
1437 1438 raise error.ParseError(_('malformed line "%s"') % r)
1438 1439 verb, rest = r.split(' ', 1)
1439 1440
1440 1441 if verb not in actiontable:
1441 1442 raise error.ParseError(_('unknown action "%s"') % verb)
1442 1443
1443 1444 action = actiontable[verb].fromrule(state, rest)
1444 1445 actions.append(action)
1445 1446 return actions
1446 1447
1447 1448 def warnverifyactions(ui, repo, actions, state, ctxs):
1448 1449 try:
1449 1450 verifyactions(actions, state, ctxs)
1450 1451 except error.ParseError:
1451 1452 if repo.vfs.exists('histedit-last-edit.txt'):
1452 1453 ui.warn(_('warning: histedit rules saved '
1453 1454 'to: .hg/histedit-last-edit.txt\n'))
1454 1455 raise
1455 1456
1456 1457 def verifyactions(actions, state, ctxs):
1457 1458 """Verify that there exists exactly one action per given changeset and
1458 1459 other constraints.
1459 1460
1460 1461 Will abort if there are to many or too few rules, a malformed rule,
1461 1462 or a rule on a changeset outside of the user-given range.
1462 1463 """
1463 1464 expected = set(c.node() for c in ctxs)
1464 1465 seen = set()
1465 1466 prev = None
1466 1467
1467 1468 if actions and actions[0].verb in ['roll', 'fold']:
1468 1469 raise error.ParseError(_('first changeset cannot use verb "%s"') %
1469 1470 actions[0].verb)
1470 1471
1471 1472 for action in actions:
1472 1473 action.verify(prev, expected, seen)
1473 1474 prev = action
1474 1475 if action.node is not None:
1475 1476 seen.add(action.node)
1476 1477 missing = sorted(expected - seen) # sort to stabilize output
1477 1478
1478 1479 if state.repo.ui.configbool('histedit', 'dropmissing'):
1479 1480 if len(actions) == 0:
1480 1481 raise error.ParseError(_('no rules provided'),
1481 1482 hint=_('use strip extension to remove commits'))
1482 1483
1483 1484 drops = [drop(state, n) for n in missing]
1484 1485 # put the in the beginning so they execute immediately and
1485 1486 # don't show in the edit-plan in the future
1486 1487 actions[:0] = drops
1487 1488 elif missing:
1488 1489 raise error.ParseError(_('missing rules for changeset %s') %
1489 1490 node.short(missing[0]),
1490 1491 hint=_('use "drop %s" to discard, see also: '
1491 1492 "'hg help -e histedit.config'")
1492 1493 % node.short(missing[0]))
1493 1494
1494 1495 def adjustreplacementsfrommarkers(repo, oldreplacements):
1495 1496 """Adjust replacements from obsolescence markers
1496 1497
1497 1498 Replacements structure is originally generated based on
1498 1499 histedit's state and does not account for changes that are
1499 1500 not recorded there. This function fixes that by adding
1500 1501 data read from obsolescence markers"""
1501 1502 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1502 1503 return oldreplacements
1503 1504
1504 1505 unfi = repo.unfiltered()
1505 1506 nm = unfi.changelog.nodemap
1506 1507 obsstore = repo.obsstore
1507 1508 newreplacements = list(oldreplacements)
1508 1509 oldsuccs = [r[1] for r in oldreplacements]
1509 1510 # successors that have already been added to succstocheck once
1510 1511 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1511 1512 succstocheck = list(seensuccs)
1512 1513 while succstocheck:
1513 1514 n = succstocheck.pop()
1514 1515 missing = nm.get(n) is None
1515 1516 markers = obsstore.successors.get(n, ())
1516 1517 if missing and not markers:
1517 1518 # dead end, mark it as such
1518 1519 newreplacements.append((n, ()))
1519 1520 for marker in markers:
1520 1521 nsuccs = marker[1]
1521 1522 newreplacements.append((n, nsuccs))
1522 1523 for nsucc in nsuccs:
1523 1524 if nsucc not in seensuccs:
1524 1525 seensuccs.add(nsucc)
1525 1526 succstocheck.append(nsucc)
1526 1527
1527 1528 return newreplacements
1528 1529
1529 1530 def processreplacement(state):
1530 1531 """process the list of replacements to return
1531 1532
1532 1533 1) the final mapping between original and created nodes
1533 1534 2) the list of temporary node created by histedit
1534 1535 3) the list of new commit created by histedit"""
1535 1536 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1536 1537 allsuccs = set()
1537 1538 replaced = set()
1538 1539 fullmapping = {}
1539 1540 # initialize basic set
1540 1541 # fullmapping records all operations recorded in replacement
1541 1542 for rep in replacements:
1542 1543 allsuccs.update(rep[1])
1543 1544 replaced.add(rep[0])
1544 1545 fullmapping.setdefault(rep[0], set()).update(rep[1])
1545 1546 new = allsuccs - replaced
1546 1547 tmpnodes = allsuccs & replaced
1547 1548 # Reduce content fullmapping into direct relation between original nodes
1548 1549 # and final node created during history edition
1549 1550 # Dropped changeset are replaced by an empty list
1550 1551 toproceed = set(fullmapping)
1551 1552 final = {}
1552 1553 while toproceed:
1553 1554 for x in list(toproceed):
1554 1555 succs = fullmapping[x]
1555 1556 for s in list(succs):
1556 1557 if s in toproceed:
1557 1558 # non final node with unknown closure
1558 1559 # We can't process this now
1559 1560 break
1560 1561 elif s in final:
1561 1562 # non final node, replace with closure
1562 1563 succs.remove(s)
1563 1564 succs.update(final[s])
1564 1565 else:
1565 1566 final[x] = succs
1566 1567 toproceed.remove(x)
1567 1568 # remove tmpnodes from final mapping
1568 1569 for n in tmpnodes:
1569 1570 del final[n]
1570 1571 # we expect all changes involved in final to exist in the repo
1571 1572 # turn `final` into list (topologically sorted)
1572 1573 nm = state.repo.changelog.nodemap
1573 1574 for prec, succs in final.items():
1574 1575 final[prec] = sorted(succs, key=nm.get)
1575 1576
1576 1577 # computed topmost element (necessary for bookmark)
1577 1578 if new:
1578 1579 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1579 1580 elif not final:
1580 1581 # Nothing rewritten at all. we won't need `newtopmost`
1581 1582 # It is the same as `oldtopmost` and `processreplacement` know it
1582 1583 newtopmost = None
1583 1584 else:
1584 1585 # every body died. The newtopmost is the parent of the root.
1585 1586 r = state.repo.changelog.rev
1586 1587 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1587 1588
1588 1589 return final, tmpnodes, new, newtopmost
1589 1590
1590 1591 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
1591 1592 """Move bookmark from oldtopmost to newly created topmost
1592 1593
1593 1594 This is arguably a feature and we may only want that for the active
1594 1595 bookmark. But the behavior is kept compatible with the old version for now.
1595 1596 """
1596 1597 if not oldtopmost or not newtopmost:
1597 1598 return
1598 1599 oldbmarks = repo.nodebookmarks(oldtopmost)
1599 1600 if oldbmarks:
1600 1601 with repo.lock(), repo.transaction('histedit') as tr:
1601 1602 marks = repo._bookmarks
1602 1603 changes = []
1603 1604 for name in oldbmarks:
1604 1605 changes.append((name, newtopmost))
1605 1606 marks.applychanges(repo, tr, changes)
1606 1607
1607 1608 def cleanupnode(ui, repo, nodes, nobackup=False):
1608 1609 """strip a group of nodes from the repository
1609 1610
1610 1611 The set of node to strip may contains unknown nodes."""
1611 1612 with repo.lock():
1612 1613 # do not let filtering get in the way of the cleanse
1613 1614 # we should probably get rid of obsolescence marker created during the
1614 1615 # histedit, but we currently do not have such information.
1615 1616 repo = repo.unfiltered()
1616 1617 # Find all nodes that need to be stripped
1617 1618 # (we use %lr instead of %ln to silently ignore unknown items)
1618 1619 nm = repo.changelog.nodemap
1619 1620 nodes = sorted(n for n in nodes if n in nm)
1620 1621 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1621 1622 if roots:
1622 1623 backup = not nobackup
1623 1624 repair.strip(ui, repo, roots, backup=backup)
1624 1625
1625 1626 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1626 1627 if isinstance(nodelist, str):
1627 1628 nodelist = [nodelist]
1628 1629 state = histeditstate(repo)
1629 1630 if state.inprogress():
1630 1631 state.read()
1631 1632 histedit_nodes = {action.node for action
1632 1633 in state.actions if action.node}
1633 1634 common_nodes = histedit_nodes & set(nodelist)
1634 1635 if common_nodes:
1635 1636 raise error.Abort(_("histedit in progress, can't strip %s")
1636 1637 % ', '.join(node.short(x) for x in common_nodes))
1637 1638 return orig(ui, repo, nodelist, *args, **kwargs)
1638 1639
1639 1640 extensions.wrapfunction(repair, 'strip', stripwrapper)
1640 1641
1641 1642 def summaryhook(ui, repo):
1642 1643 state = histeditstate(repo)
1643 1644 if not state.inprogress():
1644 1645 return
1645 1646 state.read()
1646 1647 if state.actions:
1647 1648 # i18n: column positioning for "hg summary"
1648 1649 ui.write(_('hist: %s (histedit --continue)\n') %
1649 1650 (ui.label(_('%d remaining'), 'histedit.remaining') %
1650 1651 len(state.actions)))
1651 1652
1652 1653 def extsetup(ui):
1653 1654 cmdutil.summaryhooks.add('histedit', summaryhook)
1654 1655 cmdutil.unfinishedstates.append(
1655 1656 ['histedit-state', False, True, _('histedit in progress'),
1656 1657 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1657 1658 cmdutil.afterresolvedstates.append(
1658 1659 ['histedit-state', _('hg histedit --continue')])
@@ -1,527 +1,528 b''
1 1 # journal.py
2 2 #
3 3 # Copyright 2014-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 """track previous positions of bookmarks (EXPERIMENTAL)
8 8
9 9 This extension adds a new command: `hg journal`, which shows you where
10 10 bookmarks were previously located.
11 11
12 12 """
13 13
14 14 from __future__ import absolute_import
15 15
16 16 import collections
17 17 import errno
18 18 import os
19 19 import weakref
20 20
21 21 from mercurial.i18n import _
22 22
23 23 from mercurial import (
24 24 bookmarks,
25 25 cmdutil,
26 26 dispatch,
27 27 encoding,
28 28 error,
29 29 extensions,
30 30 hg,
31 31 localrepo,
32 32 lock,
33 33 logcmdutil,
34 34 node,
35 35 pycompat,
36 36 registrar,
37 37 util,
38 38 )
39 39 from mercurial.utils import (
40 40 dateutil,
41 41 procutil,
42 42 stringutil,
43 43 )
44 44
45 45 cmdtable = {}
46 46 command = registrar.command(cmdtable)
47 47
48 48 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
49 49 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
50 50 # be specifying the version(s) of Mercurial they are tested with, or
51 51 # leave the attribute unspecified.
52 52 testedwith = 'ships-with-hg-core'
53 53
54 54 # storage format version; increment when the format changes
55 55 storageversion = 0
56 56
57 57 # namespaces
58 58 bookmarktype = 'bookmark'
59 59 wdirparenttype = 'wdirparent'
60 60 # In a shared repository, what shared feature name is used
61 61 # to indicate this namespace is shared with the source?
62 62 sharednamespaces = {
63 63 bookmarktype: hg.sharedbookmarks,
64 64 }
65 65
66 66 # Journal recording, register hooks and storage object
67 67 def extsetup(ui):
68 68 extensions.wrapfunction(dispatch, 'runcommand', runcommand)
69 69 extensions.wrapfunction(bookmarks.bmstore, '_write', recordbookmarks)
70 70 extensions.wrapfilecache(
71 71 localrepo.localrepository, 'dirstate', wrapdirstate)
72 72 extensions.wrapfunction(hg, 'postshare', wrappostshare)
73 73 extensions.wrapfunction(hg, 'copystore', unsharejournal)
74 74
75 75 def reposetup(ui, repo):
76 76 if repo.local():
77 77 repo.journal = journalstorage(repo)
78 78 repo._wlockfreeprefix.add('namejournal')
79 79
80 80 dirstate, cached = localrepo.isfilecached(repo, 'dirstate')
81 81 if cached:
82 82 # already instantiated dirstate isn't yet marked as
83 83 # "journal"-ing, even though repo.dirstate() was already
84 84 # wrapped by own wrapdirstate()
85 85 _setupdirstate(repo, dirstate)
86 86
87 87 def runcommand(orig, lui, repo, cmd, fullargs, *args):
88 88 """Track the command line options for recording in the journal"""
89 89 journalstorage.recordcommand(*fullargs)
90 90 return orig(lui, repo, cmd, fullargs, *args)
91 91
92 92 def _setupdirstate(repo, dirstate):
93 93 dirstate.journalstorage = repo.journal
94 94 dirstate.addparentchangecallback('journal', recorddirstateparents)
95 95
96 96 # hooks to record dirstate changes
97 97 def wrapdirstate(orig, repo):
98 98 """Make journal storage available to the dirstate object"""
99 99 dirstate = orig(repo)
100 100 if util.safehasattr(repo, 'journal'):
101 101 _setupdirstate(repo, dirstate)
102 102 return dirstate
103 103
104 104 def recorddirstateparents(dirstate, old, new):
105 105 """Records all dirstate parent changes in the journal."""
106 106 old = list(old)
107 107 new = list(new)
108 108 if util.safehasattr(dirstate, 'journalstorage'):
109 109 # only record two hashes if there was a merge
110 110 oldhashes = old[:1] if old[1] == node.nullid else old
111 111 newhashes = new[:1] if new[1] == node.nullid else new
112 112 dirstate.journalstorage.record(
113 113 wdirparenttype, '.', oldhashes, newhashes)
114 114
115 115 # hooks to record bookmark changes (both local and remote)
116 116 def recordbookmarks(orig, store, fp):
117 117 """Records all bookmark changes in the journal."""
118 118 repo = store._repo
119 119 if util.safehasattr(repo, 'journal'):
120 120 oldmarks = bookmarks.bmstore(repo)
121 121 for mark, value in store.iteritems():
122 122 oldvalue = oldmarks.get(mark, node.nullid)
123 123 if value != oldvalue:
124 124 repo.journal.record(bookmarktype, mark, oldvalue, value)
125 125 return orig(store, fp)
126 126
127 127 # shared repository support
128 128 def _readsharedfeatures(repo):
129 129 """A set of shared features for this repository"""
130 130 try:
131 131 return set(repo.vfs.read('shared').splitlines())
132 132 except IOError as inst:
133 133 if inst.errno != errno.ENOENT:
134 134 raise
135 135 return set()
136 136
137 137 def _mergeentriesiter(*iterables, **kwargs):
138 138 """Given a set of sorted iterables, yield the next entry in merged order
139 139
140 140 Note that by default entries go from most recent to oldest.
141 141 """
142 142 order = kwargs.pop(r'order', max)
143 143 iterables = [iter(it) for it in iterables]
144 144 # this tracks still active iterables; iterables are deleted as they are
145 145 # exhausted, which is why this is a dictionary and why each entry also
146 146 # stores the key. Entries are mutable so we can store the next value each
147 147 # time.
148 148 iterable_map = {}
149 149 for key, it in enumerate(iterables):
150 150 try:
151 151 iterable_map[key] = [next(it), key, it]
152 152 except StopIteration:
153 153 # empty entry, can be ignored
154 154 pass
155 155
156 156 while iterable_map:
157 157 value, key, it = order(iterable_map.itervalues())
158 158 yield value
159 159 try:
160 160 iterable_map[key][0] = next(it)
161 161 except StopIteration:
162 162 # this iterable is empty, remove it from consideration
163 163 del iterable_map[key]
164 164
165 165 def wrappostshare(orig, sourcerepo, destrepo, **kwargs):
166 166 """Mark this shared working copy as sharing journal information"""
167 167 with destrepo.wlock():
168 168 orig(sourcerepo, destrepo, **kwargs)
169 169 with destrepo.vfs('shared', 'a') as fp:
170 170 fp.write('journal\n')
171 171
172 172 def unsharejournal(orig, ui, repo, repopath):
173 173 """Copy shared journal entries into this repo when unsharing"""
174 174 if (repo.path == repopath and repo.shared() and
175 175 util.safehasattr(repo, 'journal')):
176 176 sharedrepo = hg.sharedreposource(repo)
177 177 sharedfeatures = _readsharedfeatures(repo)
178 178 if sharedrepo and sharedfeatures > {'journal'}:
179 179 # there is a shared repository and there are shared journal entries
180 180 # to copy. move shared date over from source to destination but
181 181 # move the local file first
182 182 if repo.vfs.exists('namejournal'):
183 183 journalpath = repo.vfs.join('namejournal')
184 184 util.rename(journalpath, journalpath + '.bak')
185 185 storage = repo.journal
186 186 local = storage._open(
187 187 repo.vfs, filename='namejournal.bak', _newestfirst=False)
188 188 shared = (
189 189 e for e in storage._open(sharedrepo.vfs, _newestfirst=False)
190 190 if sharednamespaces.get(e.namespace) in sharedfeatures)
191 191 for entry in _mergeentriesiter(local, shared, order=min):
192 192 storage._write(repo.vfs, entry)
193 193
194 194 return orig(ui, repo, repopath)
195 195
196 196 class journalentry(collections.namedtuple(
197 197 u'journalentry',
198 198 u'timestamp user command namespace name oldhashes newhashes')):
199 199 """Individual journal entry
200 200
201 201 * timestamp: a mercurial (time, timezone) tuple
202 202 * user: the username that ran the command
203 203 * namespace: the entry namespace, an opaque string
204 204 * name: the name of the changed item, opaque string with meaning in the
205 205 namespace
206 206 * command: the hg command that triggered this record
207 207 * oldhashes: a tuple of one or more binary hashes for the old location
208 208 * newhashes: a tuple of one or more binary hashes for the new location
209 209
210 210 Handles serialisation from and to the storage format. Fields are
211 211 separated by newlines, hashes are written out in hex separated by commas,
212 212 timestamp and timezone are separated by a space.
213 213
214 214 """
215 215 @classmethod
216 216 def fromstorage(cls, line):
217 217 (time, user, command, namespace, name,
218 218 oldhashes, newhashes) = line.split('\n')
219 219 timestamp, tz = time.split()
220 220 timestamp, tz = float(timestamp), int(tz)
221 221 oldhashes = tuple(node.bin(hash) for hash in oldhashes.split(','))
222 222 newhashes = tuple(node.bin(hash) for hash in newhashes.split(','))
223 223 return cls(
224 224 (timestamp, tz), user, command, namespace, name,
225 225 oldhashes, newhashes)
226 226
227 227 def __bytes__(self):
228 228 """bytes representation for storage"""
229 229 time = ' '.join(map(pycompat.bytestr, self.timestamp))
230 230 oldhashes = ','.join([node.hex(hash) for hash in self.oldhashes])
231 231 newhashes = ','.join([node.hex(hash) for hash in self.newhashes])
232 232 return '\n'.join((
233 233 time, self.user, self.command, self.namespace, self.name,
234 234 oldhashes, newhashes))
235 235
236 236 __str__ = encoding.strmethod(__bytes__)
237 237
238 238 class journalstorage(object):
239 239 """Storage for journal entries
240 240
241 241 Entries are divided over two files; one with entries that pertain to the
242 242 local working copy *only*, and one with entries that are shared across
243 243 multiple working copies when shared using the share extension.
244 244
245 245 Entries are stored with NUL bytes as separators. See the journalentry
246 246 class for the per-entry structure.
247 247
248 248 The file format starts with an integer version, delimited by a NUL.
249 249
250 250 This storage uses a dedicated lock; this makes it easier to avoid issues
251 251 with adding entries that added when the regular wlock is unlocked (e.g.
252 252 the dirstate).
253 253
254 254 """
255 255 _currentcommand = ()
256 256 _lockref = None
257 257
258 258 def __init__(self, repo):
259 259 self.user = procutil.getuser()
260 260 self.ui = repo.ui
261 261 self.vfs = repo.vfs
262 262
263 263 # is this working copy using a shared storage?
264 264 self.sharedfeatures = self.sharedvfs = None
265 265 if repo.shared():
266 266 features = _readsharedfeatures(repo)
267 267 sharedrepo = hg.sharedreposource(repo)
268 268 if sharedrepo is not None and 'journal' in features:
269 269 self.sharedvfs = sharedrepo.vfs
270 270 self.sharedfeatures = features
271 271
272 272 # track the current command for recording in journal entries
273 273 @property
274 274 def command(self):
275 275 commandstr = ' '.join(
276 276 map(procutil.shellquote, journalstorage._currentcommand))
277 277 if '\n' in commandstr:
278 278 # truncate multi-line commands
279 279 commandstr = commandstr.partition('\n')[0] + ' ...'
280 280 return commandstr
281 281
282 282 @classmethod
283 283 def recordcommand(cls, *fullargs):
284 284 """Set the current hg arguments, stored with recorded entries"""
285 285 # Set the current command on the class because we may have started
286 286 # with a non-local repo (cloning for example).
287 287 cls._currentcommand = fullargs
288 288
289 289 def _currentlock(self, lockref):
290 290 """Returns the lock if it's held, or None if it's not.
291 291
292 292 (This is copied from the localrepo class)
293 293 """
294 294 if lockref is None:
295 295 return None
296 296 l = lockref()
297 297 if l is None or not l.held:
298 298 return None
299 299 return l
300 300
301 301 def jlock(self, vfs):
302 302 """Create a lock for the journal file"""
303 303 if self._currentlock(self._lockref) is not None:
304 304 raise error.Abort(_('journal lock does not support nesting'))
305 305 desc = _('journal of %s') % vfs.base
306 306 try:
307 307 l = lock.lock(vfs, 'namejournal.lock', 0, desc=desc)
308 308 except error.LockHeld as inst:
309 309 self.ui.warn(
310 310 _("waiting for lock on %s held by %r\n") % (desc, inst.locker))
311 311 # default to 600 seconds timeout
312 312 l = lock.lock(
313 313 vfs, 'namejournal.lock',
314 314 self.ui.configint("ui", "timeout"), desc=desc)
315 315 self.ui.warn(_("got lock after %s seconds\n") % l.delay)
316 316 self._lockref = weakref.ref(l)
317 317 return l
318 318
319 319 def record(self, namespace, name, oldhashes, newhashes):
320 320 """Record a new journal entry
321 321
322 322 * namespace: an opaque string; this can be used to filter on the type
323 323 of recorded entries.
324 324 * name: the name defining this entry; for bookmarks, this is the
325 325 bookmark name. Can be filtered on when retrieving entries.
326 326 * oldhashes and newhashes: each a single binary hash, or a list of
327 327 binary hashes. These represent the old and new position of the named
328 328 item.
329 329
330 330 """
331 331 if not isinstance(oldhashes, list):
332 332 oldhashes = [oldhashes]
333 333 if not isinstance(newhashes, list):
334 334 newhashes = [newhashes]
335 335
336 336 entry = journalentry(
337 337 dateutil.makedate(), self.user, self.command, namespace, name,
338 338 oldhashes, newhashes)
339 339
340 340 vfs = self.vfs
341 341 if self.sharedvfs is not None:
342 342 # write to the shared repository if this feature is being
343 343 # shared between working copies.
344 344 if sharednamespaces.get(namespace) in self.sharedfeatures:
345 345 vfs = self.sharedvfs
346 346
347 347 self._write(vfs, entry)
348 348
349 349 def _write(self, vfs, entry):
350 350 with self.jlock(vfs):
351 351 version = None
352 352 # open file in amend mode to ensure it is created if missing
353 353 with vfs('namejournal', mode='a+b') as f:
354 354 f.seek(0, os.SEEK_SET)
355 355 # Read just enough bytes to get a version number (up to 2
356 356 # digits plus separator)
357 357 version = f.read(3).partition('\0')[0]
358 358 if version and version != "%d" % storageversion:
359 359 # different version of the storage. Exit early (and not
360 360 # write anything) if this is not a version we can handle or
361 361 # the file is corrupt. In future, perhaps rotate the file
362 362 # instead?
363 363 self.ui.warn(
364 364 _("unsupported journal file version '%s'\n") % version)
365 365 return
366 366 if not version:
367 367 # empty file, write version first
368 368 f.write(("%d" % storageversion) + '\0')
369 369 f.seek(0, os.SEEK_END)
370 370 f.write(bytes(entry) + '\0')
371 371
372 372 def filtered(self, namespace=None, name=None):
373 373 """Yield all journal entries with the given namespace or name
374 374
375 375 Both the namespace and the name are optional; if neither is given all
376 376 entries in the journal are produced.
377 377
378 378 Matching supports regular expressions by using the `re:` prefix
379 379 (use `literal:` to match names or namespaces that start with `re:`)
380 380
381 381 """
382 382 if namespace is not None:
383 383 namespace = stringutil.stringmatcher(namespace)[-1]
384 384 if name is not None:
385 385 name = stringutil.stringmatcher(name)[-1]
386 386 for entry in self:
387 387 if namespace is not None and not namespace(entry.namespace):
388 388 continue
389 389 if name is not None and not name(entry.name):
390 390 continue
391 391 yield entry
392 392
393 393 def __iter__(self):
394 394 """Iterate over the storage
395 395
396 396 Yields journalentry instances for each contained journal record.
397 397
398 398 """
399 399 local = self._open(self.vfs)
400 400
401 401 if self.sharedvfs is None:
402 402 return local
403 403
404 404 # iterate over both local and shared entries, but only those
405 405 # shared entries that are among the currently shared features
406 406 shared = (
407 407 e for e in self._open(self.sharedvfs)
408 408 if sharednamespaces.get(e.namespace) in self.sharedfeatures)
409 409 return _mergeentriesiter(local, shared)
410 410
411 411 def _open(self, vfs, filename='namejournal', _newestfirst=True):
412 412 if not vfs.exists(filename):
413 413 return
414 414
415 415 with vfs(filename) as f:
416 416 raw = f.read()
417 417
418 418 lines = raw.split('\0')
419 419 version = lines and lines[0]
420 420 if version != "%d" % storageversion:
421 421 version = version or _('not available')
422 422 raise error.Abort(_("unknown journal file version '%s'") % version)
423 423
424 424 # Skip the first line, it's a version number. Normally we iterate over
425 425 # these in reverse order to list newest first; only when copying across
426 426 # a shared storage do we forgo reversing.
427 427 lines = lines[1:]
428 428 if _newestfirst:
429 429 lines = reversed(lines)
430 430 for line in lines:
431 431 if not line:
432 432 continue
433 433 yield journalentry.fromstorage(line)
434 434
435 435 # journal reading
436 436 # log options that don't make sense for journal
437 437 _ignoreopts = ('no-merges', 'graph')
438 438 @command(
439 439 'journal', [
440 440 ('', 'all', None, 'show history for all names'),
441 441 ('c', 'commits', None, 'show commit metadata'),
442 442 ] + [opt for opt in cmdutil.logopts if opt[1] not in _ignoreopts],
443 '[OPTION]... [BOOKMARKNAME]')
443 '[OPTION]... [BOOKMARKNAME]',
444 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
444 445 def journal(ui, repo, *args, **opts):
445 446 """show the previous position of bookmarks and the working copy
446 447
447 448 The journal is used to see the previous commits that bookmarks and the
448 449 working copy pointed to. By default the previous locations for the working
449 450 copy. Passing a bookmark name will show all the previous positions of
450 451 that bookmark. Use the --all switch to show previous locations for all
451 452 bookmarks and the working copy; each line will then include the bookmark
452 453 name, or '.' for the working copy, as well.
453 454
454 455 If `name` starts with `re:`, the remainder of the name is treated as
455 456 a regular expression. To match a name that actually starts with `re:`,
456 457 use the prefix `literal:`.
457 458
458 459 By default hg journal only shows the commit hash and the command that was
459 460 running at that time. -v/--verbose will show the prior hash, the user, and
460 461 the time at which it happened.
461 462
462 463 Use -c/--commits to output log information on each commit hash; at this
463 464 point you can use the usual `--patch`, `--git`, `--stat` and `--template`
464 465 switches to alter the log output for these.
465 466
466 467 `hg journal -T json` can be used to produce machine readable output.
467 468
468 469 """
469 470 opts = pycompat.byteskwargs(opts)
470 471 name = '.'
471 472 if opts.get('all'):
472 473 if args:
473 474 raise error.Abort(
474 475 _("You can't combine --all and filtering on a name"))
475 476 name = None
476 477 if args:
477 478 name = args[0]
478 479
479 480 fm = ui.formatter('journal', opts)
480 481 def formatnodes(nodes):
481 482 return fm.formatlist(map(fm.hexfunc, nodes), name='node', sep=',')
482 483
483 484 if opts.get("template") != "json":
484 485 if name is None:
485 486 displayname = _('the working copy and bookmarks')
486 487 else:
487 488 displayname = "'%s'" % name
488 489 ui.status(_("previous locations of %s:\n") % displayname)
489 490
490 491 limit = logcmdutil.getlimit(opts)
491 492 entry = None
492 493 ui.pager('journal')
493 494 for count, entry in enumerate(repo.journal.filtered(name=name)):
494 495 if count == limit:
495 496 break
496 497
497 498 fm.startitem()
498 499 fm.condwrite(ui.verbose, 'oldnodes', '%s -> ',
499 500 formatnodes(entry.oldhashes))
500 501 fm.write('newnodes', '%s', formatnodes(entry.newhashes))
501 502 fm.condwrite(ui.verbose, 'user', ' %-8s', entry.user)
502 503 fm.condwrite(
503 504 opts.get('all') or name.startswith('re:'),
504 505 'name', ' %-8s', entry.name)
505 506
506 507 fm.condwrite(ui.verbose, 'date', ' %s',
507 508 fm.formatdate(entry.timestamp, '%Y-%m-%d %H:%M %1%2'))
508 509 fm.write('command', ' %s\n', entry.command)
509 510
510 511 if opts.get("commits"):
511 512 if fm.isplain():
512 513 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
513 514 else:
514 515 displayer = logcmdutil.changesetformatter(
515 516 ui, repo, fm.nested('changesets'), diffopts=opts)
516 517 for hash in entry.newhashes:
517 518 try:
518 519 ctx = repo[hash]
519 520 displayer.show(ctx)
520 521 except error.RepoLookupError as e:
521 522 fm.plain("%s\n\n" % pycompat.bytestr(e))
522 523 displayer.close()
523 524
524 525 fm.end()
525 526
526 527 if entry is None:
527 528 ui.status(_("no recorded locations\n"))
@@ -1,3670 +1,3696 b''
1 1 # mq.py - patch queues for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
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 '''manage a stack of patches
9 9
10 10 This extension lets you work with a stack of patches in a Mercurial
11 11 repository. It manages two stacks of patches - all known patches, and
12 12 applied patches (subset of known patches).
13 13
14 14 Known patches are represented as patch files in the .hg/patches
15 15 directory. Applied patches are both patch files and changesets.
16 16
17 17 Common tasks (use :hg:`help COMMAND` for more details)::
18 18
19 19 create new patch qnew
20 20 import existing patch qimport
21 21
22 22 print patch series qseries
23 23 print applied patches qapplied
24 24
25 25 add known patch to applied stack qpush
26 26 remove patch from applied stack qpop
27 27 refresh contents of top applied patch qrefresh
28 28
29 29 By default, mq will automatically use git patches when required to
30 30 avoid losing file mode changes, copy records, binary files or empty
31 31 files creations or deletions. This behavior can be configured with::
32 32
33 33 [mq]
34 34 git = auto/keep/yes/no
35 35
36 36 If set to 'keep', mq will obey the [diff] section configuration while
37 37 preserving existing git patches upon qrefresh. If set to 'yes' or
38 38 'no', mq will override the [diff] section and always generate git or
39 39 regular patches, possibly losing data in the second case.
40 40
41 41 It may be desirable for mq changesets to be kept in the secret phase (see
42 42 :hg:`help phases`), which can be enabled with the following setting::
43 43
44 44 [mq]
45 45 secret = True
46 46
47 47 You will by default be managing a patch queue named "patches". You can
48 48 create other, independent patch queues with the :hg:`qqueue` command.
49 49
50 50 If the working directory contains uncommitted files, qpush, qpop and
51 51 qgoto abort immediately. If -f/--force is used, the changes are
52 52 discarded. Setting::
53 53
54 54 [mq]
55 55 keepchanges = True
56 56
57 57 make them behave as if --keep-changes were passed, and non-conflicting
58 58 local changes will be tolerated and preserved. If incompatible options
59 59 such as -f/--force or --exact are passed, this setting is ignored.
60 60
61 61 This extension used to provide a strip command. This command now lives
62 62 in the strip extension.
63 63 '''
64 64
65 65 from __future__ import absolute_import, print_function
66 66
67 67 import errno
68 68 import os
69 69 import re
70 70 import shutil
71 71 from mercurial.i18n import _
72 72 from mercurial.node import (
73 73 bin,
74 74 hex,
75 75 nullid,
76 76 nullrev,
77 77 short,
78 78 )
79 79 from mercurial import (
80 80 cmdutil,
81 81 commands,
82 82 dirstateguard,
83 83 encoding,
84 84 error,
85 85 extensions,
86 86 hg,
87 87 localrepo,
88 88 lock as lockmod,
89 89 logcmdutil,
90 90 patch as patchmod,
91 91 phases,
92 92 pycompat,
93 93 registrar,
94 94 revsetlang,
95 95 scmutil,
96 96 smartset,
97 97 subrepoutil,
98 98 util,
99 99 vfs as vfsmod,
100 100 )
101 101 from mercurial.utils import (
102 102 dateutil,
103 103 stringutil,
104 104 )
105 105
106 106 release = lockmod.release
107 107 seriesopts = [('s', 'summary', None, _('print first line of patch header'))]
108 108
109 109 cmdtable = {}
110 110 command = registrar.command(cmdtable)
111 111 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
112 112 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
113 113 # be specifying the version(s) of Mercurial they are tested with, or
114 114 # leave the attribute unspecified.
115 115 testedwith = 'ships-with-hg-core'
116 116
117 117 configtable = {}
118 118 configitem = registrar.configitem(configtable)
119 119
120 120 configitem('mq', 'git',
121 121 default='auto',
122 122 )
123 123 configitem('mq', 'keepchanges',
124 124 default=False,
125 125 )
126 126 configitem('mq', 'plain',
127 127 default=False,
128 128 )
129 129 configitem('mq', 'secret',
130 130 default=False,
131 131 )
132 132
133 133 # force load strip extension formerly included in mq and import some utility
134 134 try:
135 135 stripext = extensions.find('strip')
136 136 except KeyError:
137 137 # note: load is lazy so we could avoid the try-except,
138 138 # but I (marmoute) prefer this explicit code.
139 139 class dummyui(object):
140 140 def debug(self, msg):
141 141 pass
142 142 stripext = extensions.load(dummyui(), 'strip', '')
143 143
144 144 strip = stripext.strip
145 145 checksubstate = stripext.checksubstate
146 146 checklocalchanges = stripext.checklocalchanges
147 147
148 148
149 149 # Patch names looks like unix-file names.
150 150 # They must be joinable with queue directory and result in the patch path.
151 151 normname = util.normpath
152 152
153 153 class statusentry(object):
154 154 def __init__(self, node, name):
155 155 self.node, self.name = node, name
156 156
157 157 def __bytes__(self):
158 158 return hex(self.node) + ':' + self.name
159 159
160 160 __str__ = encoding.strmethod(__bytes__)
161 161 __repr__ = encoding.strmethod(__bytes__)
162 162
163 163 # The order of the headers in 'hg export' HG patches:
164 164 HGHEADERS = [
165 165 # '# HG changeset patch',
166 166 '# User ',
167 167 '# Date ',
168 168 '# ',
169 169 '# Branch ',
170 170 '# Node ID ',
171 171 '# Parent ', # can occur twice for merges - but that is not relevant for mq
172 172 ]
173 173 # The order of headers in plain 'mail style' patches:
174 174 PLAINHEADERS = {
175 175 'from': 0,
176 176 'date': 1,
177 177 'subject': 2,
178 178 }
179 179
180 180 def inserthgheader(lines, header, value):
181 181 """Assuming lines contains a HG patch header, add a header line with value.
182 182 >>> try: inserthgheader([], b'# Date ', b'z')
183 183 ... except ValueError as inst: print("oops")
184 184 oops
185 185 >>> inserthgheader([b'# HG changeset patch'], b'# Date ', b'z')
186 186 ['# HG changeset patch', '# Date z']
187 187 >>> inserthgheader([b'# HG changeset patch', b''], b'# Date ', b'z')
188 188 ['# HG changeset patch', '# Date z', '']
189 189 >>> inserthgheader([b'# HG changeset patch', b'# User y'], b'# Date ', b'z')
190 190 ['# HG changeset patch', '# User y', '# Date z']
191 191 >>> inserthgheader([b'# HG changeset patch', b'# Date x', b'# User y'],
192 192 ... b'# User ', b'z')
193 193 ['# HG changeset patch', '# Date x', '# User z']
194 194 >>> inserthgheader([b'# HG changeset patch', b'# Date y'], b'# Date ', b'z')
195 195 ['# HG changeset patch', '# Date z']
196 196 >>> inserthgheader([b'# HG changeset patch', b'', b'# Date y'],
197 197 ... b'# Date ', b'z')
198 198 ['# HG changeset patch', '# Date z', '', '# Date y']
199 199 >>> inserthgheader([b'# HG changeset patch', b'# Parent y'],
200 200 ... b'# Date ', b'z')
201 201 ['# HG changeset patch', '# Date z', '# Parent y']
202 202 """
203 203 start = lines.index('# HG changeset patch') + 1
204 204 newindex = HGHEADERS.index(header)
205 205 bestpos = len(lines)
206 206 for i in range(start, len(lines)):
207 207 line = lines[i]
208 208 if not line.startswith('# '):
209 209 bestpos = min(bestpos, i)
210 210 break
211 211 for lineindex, h in enumerate(HGHEADERS):
212 212 if line.startswith(h):
213 213 if lineindex == newindex:
214 214 lines[i] = header + value
215 215 return lines
216 216 if lineindex > newindex:
217 217 bestpos = min(bestpos, i)
218 218 break # next line
219 219 lines.insert(bestpos, header + value)
220 220 return lines
221 221
222 222 def insertplainheader(lines, header, value):
223 223 """For lines containing a plain patch header, add a header line with value.
224 224 >>> insertplainheader([], b'Date', b'z')
225 225 ['Date: z']
226 226 >>> insertplainheader([b''], b'Date', b'z')
227 227 ['Date: z', '']
228 228 >>> insertplainheader([b'x'], b'Date', b'z')
229 229 ['Date: z', '', 'x']
230 230 >>> insertplainheader([b'From: y', b'x'], b'Date', b'z')
231 231 ['From: y', 'Date: z', '', 'x']
232 232 >>> insertplainheader([b' date : x', b' from : y', b''], b'From', b'z')
233 233 [' date : x', 'From: z', '']
234 234 >>> insertplainheader([b'', b'Date: y'], b'Date', b'z')
235 235 ['Date: z', '', 'Date: y']
236 236 >>> insertplainheader([b'foo: bar', b'DATE: z', b'x'], b'From', b'y')
237 237 ['From: y', 'foo: bar', 'DATE: z', '', 'x']
238 238 """
239 239 newprio = PLAINHEADERS[header.lower()]
240 240 bestpos = len(lines)
241 241 for i, line in enumerate(lines):
242 242 if ':' in line:
243 243 lheader = line.split(':', 1)[0].strip().lower()
244 244 lprio = PLAINHEADERS.get(lheader, newprio + 1)
245 245 if lprio == newprio:
246 246 lines[i] = '%s: %s' % (header, value)
247 247 return lines
248 248 if lprio > newprio and i < bestpos:
249 249 bestpos = i
250 250 else:
251 251 if line:
252 252 lines.insert(i, '')
253 253 if i < bestpos:
254 254 bestpos = i
255 255 break
256 256 lines.insert(bestpos, '%s: %s' % (header, value))
257 257 return lines
258 258
259 259 class patchheader(object):
260 260 def __init__(self, pf, plainmode=False):
261 261 def eatdiff(lines):
262 262 while lines:
263 263 l = lines[-1]
264 264 if (l.startswith("diff -") or
265 265 l.startswith("Index:") or
266 266 l.startswith("===========")):
267 267 del lines[-1]
268 268 else:
269 269 break
270 270 def eatempty(lines):
271 271 while lines:
272 272 if not lines[-1].strip():
273 273 del lines[-1]
274 274 else:
275 275 break
276 276
277 277 message = []
278 278 comments = []
279 279 user = None
280 280 date = None
281 281 parent = None
282 282 format = None
283 283 subject = None
284 284 branch = None
285 285 nodeid = None
286 286 diffstart = 0
287 287
288 288 for line in open(pf, 'rb'):
289 289 line = line.rstrip()
290 290 if (line.startswith('diff --git')
291 291 or (diffstart and line.startswith('+++ '))):
292 292 diffstart = 2
293 293 break
294 294 diffstart = 0 # reset
295 295 if line.startswith("--- "):
296 296 diffstart = 1
297 297 continue
298 298 elif format == "hgpatch":
299 299 # parse values when importing the result of an hg export
300 300 if line.startswith("# User "):
301 301 user = line[7:]
302 302 elif line.startswith("# Date "):
303 303 date = line[7:]
304 304 elif line.startswith("# Parent "):
305 305 parent = line[9:].lstrip() # handle double trailing space
306 306 elif line.startswith("# Branch "):
307 307 branch = line[9:]
308 308 elif line.startswith("# Node ID "):
309 309 nodeid = line[10:]
310 310 elif not line.startswith("# ") and line:
311 311 message.append(line)
312 312 format = None
313 313 elif line == '# HG changeset patch':
314 314 message = []
315 315 format = "hgpatch"
316 316 elif (format != "tagdone" and (line.startswith("Subject: ") or
317 317 line.startswith("subject: "))):
318 318 subject = line[9:]
319 319 format = "tag"
320 320 elif (format != "tagdone" and (line.startswith("From: ") or
321 321 line.startswith("from: "))):
322 322 user = line[6:]
323 323 format = "tag"
324 324 elif (format != "tagdone" and (line.startswith("Date: ") or
325 325 line.startswith("date: "))):
326 326 date = line[6:]
327 327 format = "tag"
328 328 elif format == "tag" and line == "":
329 329 # when looking for tags (subject: from: etc) they
330 330 # end once you find a blank line in the source
331 331 format = "tagdone"
332 332 elif message or line:
333 333 message.append(line)
334 334 comments.append(line)
335 335
336 336 eatdiff(message)
337 337 eatdiff(comments)
338 338 # Remember the exact starting line of the patch diffs before consuming
339 339 # empty lines, for external use by TortoiseHg and others
340 340 self.diffstartline = len(comments)
341 341 eatempty(message)
342 342 eatempty(comments)
343 343
344 344 # make sure message isn't empty
345 345 if format and format.startswith("tag") and subject:
346 346 message.insert(0, subject)
347 347
348 348 self.message = message
349 349 self.comments = comments
350 350 self.user = user
351 351 self.date = date
352 352 self.parent = parent
353 353 # nodeid and branch are for external use by TortoiseHg and others
354 354 self.nodeid = nodeid
355 355 self.branch = branch
356 356 self.haspatch = diffstart > 1
357 357 self.plainmode = (plainmode or
358 358 '# HG changeset patch' not in self.comments and
359 359 any(c.startswith('Date: ') or
360 360 c.startswith('From: ')
361 361 for c in self.comments))
362 362
363 363 def setuser(self, user):
364 364 try:
365 365 inserthgheader(self.comments, '# User ', user)
366 366 except ValueError:
367 367 if self.plainmode:
368 368 insertplainheader(self.comments, 'From', user)
369 369 else:
370 370 tmp = ['# HG changeset patch', '# User ' + user]
371 371 self.comments = tmp + self.comments
372 372 self.user = user
373 373
374 374 def setdate(self, date):
375 375 try:
376 376 inserthgheader(self.comments, '# Date ', date)
377 377 except ValueError:
378 378 if self.plainmode:
379 379 insertplainheader(self.comments, 'Date', date)
380 380 else:
381 381 tmp = ['# HG changeset patch', '# Date ' + date]
382 382 self.comments = tmp + self.comments
383 383 self.date = date
384 384
385 385 def setparent(self, parent):
386 386 try:
387 387 inserthgheader(self.comments, '# Parent ', parent)
388 388 except ValueError:
389 389 if not self.plainmode:
390 390 tmp = ['# HG changeset patch', '# Parent ' + parent]
391 391 self.comments = tmp + self.comments
392 392 self.parent = parent
393 393
394 394 def setmessage(self, message):
395 395 if self.comments:
396 396 self._delmsg()
397 397 self.message = [message]
398 398 if message:
399 399 if self.plainmode and self.comments and self.comments[-1]:
400 400 self.comments.append('')
401 401 self.comments.append(message)
402 402
403 403 def __bytes__(self):
404 404 s = '\n'.join(self.comments).rstrip()
405 405 if not s:
406 406 return ''
407 407 return s + '\n\n'
408 408
409 409 __str__ = encoding.strmethod(__bytes__)
410 410
411 411 def _delmsg(self):
412 412 '''Remove existing message, keeping the rest of the comments fields.
413 413 If comments contains 'subject: ', message will prepend
414 414 the field and a blank line.'''
415 415 if self.message:
416 416 subj = 'subject: ' + self.message[0].lower()
417 417 for i in pycompat.xrange(len(self.comments)):
418 418 if subj == self.comments[i].lower():
419 419 del self.comments[i]
420 420 self.message = self.message[2:]
421 421 break
422 422 ci = 0
423 423 for mi in self.message:
424 424 while mi != self.comments[ci]:
425 425 ci += 1
426 426 del self.comments[ci]
427 427
428 428 def newcommit(repo, phase, *args, **kwargs):
429 429 """helper dedicated to ensure a commit respect mq.secret setting
430 430
431 431 It should be used instead of repo.commit inside the mq source for operation
432 432 creating new changeset.
433 433 """
434 434 repo = repo.unfiltered()
435 435 if phase is None:
436 436 if repo.ui.configbool('mq', 'secret'):
437 437 phase = phases.secret
438 438 overrides = {('ui', 'allowemptycommit'): True}
439 439 if phase is not None:
440 440 overrides[('phases', 'new-commit')] = phase
441 441 with repo.ui.configoverride(overrides, 'mq'):
442 442 repo.ui.setconfig('ui', 'allowemptycommit', True)
443 443 return repo.commit(*args, **kwargs)
444 444
445 445 class AbortNoCleanup(error.Abort):
446 446 pass
447 447
448 448 class queue(object):
449 449 def __init__(self, ui, baseui, path, patchdir=None):
450 450 self.basepath = path
451 451 try:
452 452 with open(os.path.join(path, 'patches.queue'), r'rb') as fh:
453 453 cur = fh.read().rstrip()
454 454
455 455 if not cur:
456 456 curpath = os.path.join(path, 'patches')
457 457 else:
458 458 curpath = os.path.join(path, 'patches-' + cur)
459 459 except IOError:
460 460 curpath = os.path.join(path, 'patches')
461 461 self.path = patchdir or curpath
462 462 self.opener = vfsmod.vfs(self.path)
463 463 self.ui = ui
464 464 self.baseui = baseui
465 465 self.applieddirty = False
466 466 self.seriesdirty = False
467 467 self.added = []
468 468 self.seriespath = "series"
469 469 self.statuspath = "status"
470 470 self.guardspath = "guards"
471 471 self.activeguards = None
472 472 self.guardsdirty = False
473 473 # Handle mq.git as a bool with extended values
474 474 gitmode = ui.config('mq', 'git').lower()
475 475 boolmode = stringutil.parsebool(gitmode)
476 476 if boolmode is not None:
477 477 if boolmode:
478 478 gitmode = 'yes'
479 479 else:
480 480 gitmode = 'no'
481 481 self.gitmode = gitmode
482 482 # deprecated config: mq.plain
483 483 self.plainmode = ui.configbool('mq', 'plain')
484 484 self.checkapplied = True
485 485
486 486 @util.propertycache
487 487 def applied(self):
488 488 def parselines(lines):
489 489 for l in lines:
490 490 entry = l.split(':', 1)
491 491 if len(entry) > 1:
492 492 n, name = entry
493 493 yield statusentry(bin(n), name)
494 494 elif l.strip():
495 495 self.ui.warn(_('malformated mq status line: %s\n') %
496 496 stringutil.pprint(entry))
497 497 # else we ignore empty lines
498 498 try:
499 499 lines = self.opener.read(self.statuspath).splitlines()
500 500 return list(parselines(lines))
501 501 except IOError as e:
502 502 if e.errno == errno.ENOENT:
503 503 return []
504 504 raise
505 505
506 506 @util.propertycache
507 507 def fullseries(self):
508 508 try:
509 509 return self.opener.read(self.seriespath).splitlines()
510 510 except IOError as e:
511 511 if e.errno == errno.ENOENT:
512 512 return []
513 513 raise
514 514
515 515 @util.propertycache
516 516 def series(self):
517 517 self.parseseries()
518 518 return self.series
519 519
520 520 @util.propertycache
521 521 def seriesguards(self):
522 522 self.parseseries()
523 523 return self.seriesguards
524 524
525 525 def invalidate(self):
526 526 for a in 'applied fullseries series seriesguards'.split():
527 527 if a in self.__dict__:
528 528 delattr(self, a)
529 529 self.applieddirty = False
530 530 self.seriesdirty = False
531 531 self.guardsdirty = False
532 532 self.activeguards = None
533 533
534 534 def diffopts(self, opts=None, patchfn=None, plain=False):
535 535 """Return diff options tweaked for this mq use, possibly upgrading to
536 536 git format, and possibly plain and without lossy options."""
537 537 diffopts = patchmod.difffeatureopts(self.ui, opts,
538 538 git=True, whitespace=not plain, formatchanging=not plain)
539 539 if self.gitmode == 'auto':
540 540 diffopts.upgrade = True
541 541 elif self.gitmode == 'keep':
542 542 pass
543 543 elif self.gitmode in ('yes', 'no'):
544 544 diffopts.git = self.gitmode == 'yes'
545 545 else:
546 546 raise error.Abort(_('mq.git option can be auto/keep/yes/no'
547 547 ' got %s') % self.gitmode)
548 548 if patchfn:
549 549 diffopts = self.patchopts(diffopts, patchfn)
550 550 return diffopts
551 551
552 552 def patchopts(self, diffopts, *patches):
553 553 """Return a copy of input diff options with git set to true if
554 554 referenced patch is a git patch and should be preserved as such.
555 555 """
556 556 diffopts = diffopts.copy()
557 557 if not diffopts.git and self.gitmode == 'keep':
558 558 for patchfn in patches:
559 559 patchf = self.opener(patchfn, 'r')
560 560 # if the patch was a git patch, refresh it as a git patch
561 561 diffopts.git = any(line.startswith('diff --git')
562 562 for line in patchf)
563 563 patchf.close()
564 564 return diffopts
565 565
566 566 def join(self, *p):
567 567 return os.path.join(self.path, *p)
568 568
569 569 def findseries(self, patch):
570 570 def matchpatch(l):
571 571 l = l.split('#', 1)[0]
572 572 return l.strip() == patch
573 573 for index, l in enumerate(self.fullseries):
574 574 if matchpatch(l):
575 575 return index
576 576 return None
577 577
578 578 guard_re = re.compile(br'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
579 579
580 580 def parseseries(self):
581 581 self.series = []
582 582 self.seriesguards = []
583 583 for l in self.fullseries:
584 584 h = l.find('#')
585 585 if h == -1:
586 586 patch = l
587 587 comment = ''
588 588 elif h == 0:
589 589 continue
590 590 else:
591 591 patch = l[:h]
592 592 comment = l[h:]
593 593 patch = patch.strip()
594 594 if patch:
595 595 if patch in self.series:
596 596 raise error.Abort(_('%s appears more than once in %s') %
597 597 (patch, self.join(self.seriespath)))
598 598 self.series.append(patch)
599 599 self.seriesguards.append(self.guard_re.findall(comment))
600 600
601 601 def checkguard(self, guard):
602 602 if not guard:
603 603 return _('guard cannot be an empty string')
604 604 bad_chars = '# \t\r\n\f'
605 605 first = guard[0]
606 606 if first in '-+':
607 607 return (_('guard %r starts with invalid character: %r') %
608 608 (guard, first))
609 609 for c in bad_chars:
610 610 if c in guard:
611 611 return _('invalid character in guard %r: %r') % (guard, c)
612 612
613 613 def setactive(self, guards):
614 614 for guard in guards:
615 615 bad = self.checkguard(guard)
616 616 if bad:
617 617 raise error.Abort(bad)
618 618 guards = sorted(set(guards))
619 619 self.ui.debug('active guards: %s\n' % ' '.join(guards))
620 620 self.activeguards = guards
621 621 self.guardsdirty = True
622 622
623 623 def active(self):
624 624 if self.activeguards is None:
625 625 self.activeguards = []
626 626 try:
627 627 guards = self.opener.read(self.guardspath).split()
628 628 except IOError as err:
629 629 if err.errno != errno.ENOENT:
630 630 raise
631 631 guards = []
632 632 for i, guard in enumerate(guards):
633 633 bad = self.checkguard(guard)
634 634 if bad:
635 635 self.ui.warn('%s:%d: %s\n' %
636 636 (self.join(self.guardspath), i + 1, bad))
637 637 else:
638 638 self.activeguards.append(guard)
639 639 return self.activeguards
640 640
641 641 def setguards(self, idx, guards):
642 642 for g in guards:
643 643 if len(g) < 2:
644 644 raise error.Abort(_('guard %r too short') % g)
645 645 if g[0] not in '-+':
646 646 raise error.Abort(_('guard %r starts with invalid char') % g)
647 647 bad = self.checkguard(g[1:])
648 648 if bad:
649 649 raise error.Abort(bad)
650 650 drop = self.guard_re.sub('', self.fullseries[idx])
651 651 self.fullseries[idx] = drop + ''.join([' #' + g for g in guards])
652 652 self.parseseries()
653 653 self.seriesdirty = True
654 654
655 655 def pushable(self, idx):
656 656 if isinstance(idx, bytes):
657 657 idx = self.series.index(idx)
658 658 patchguards = self.seriesguards[idx]
659 659 if not patchguards:
660 660 return True, None
661 661 guards = self.active()
662 662 exactneg = [g for g in patchguards
663 663 if g.startswith('-') and g[1:] in guards]
664 664 if exactneg:
665 665 return False, stringutil.pprint(exactneg[0])
666 666 pos = [g for g in patchguards if g.startswith('+')]
667 667 exactpos = [g for g in pos if g[1:] in guards]
668 668 if pos:
669 669 if exactpos:
670 670 return True, stringutil.pprint(exactpos[0])
671 671 return False, ' '.join([stringutil.pprint(p) for p in pos])
672 672 return True, ''
673 673
674 674 def explainpushable(self, idx, all_patches=False):
675 675 if all_patches:
676 676 write = self.ui.write
677 677 else:
678 678 write = self.ui.warn
679 679
680 680 if all_patches or self.ui.verbose:
681 681 if isinstance(idx, bytes):
682 682 idx = self.series.index(idx)
683 683 pushable, why = self.pushable(idx)
684 684 if all_patches and pushable:
685 685 if why is None:
686 686 write(_('allowing %s - no guards in effect\n') %
687 687 self.series[idx])
688 688 else:
689 689 if not why:
690 690 write(_('allowing %s - no matching negative guards\n') %
691 691 self.series[idx])
692 692 else:
693 693 write(_('allowing %s - guarded by %s\n') %
694 694 (self.series[idx], why))
695 695 if not pushable:
696 696 if why:
697 697 write(_('skipping %s - guarded by %s\n') %
698 698 (self.series[idx], why))
699 699 else:
700 700 write(_('skipping %s - no matching guards\n') %
701 701 self.series[idx])
702 702
703 703 def savedirty(self):
704 704 def writelist(items, path):
705 705 fp = self.opener(path, 'wb')
706 706 for i in items:
707 707 fp.write("%s\n" % i)
708 708 fp.close()
709 709 if self.applieddirty:
710 710 writelist(map(bytes, self.applied), self.statuspath)
711 711 self.applieddirty = False
712 712 if self.seriesdirty:
713 713 writelist(self.fullseries, self.seriespath)
714 714 self.seriesdirty = False
715 715 if self.guardsdirty:
716 716 writelist(self.activeguards, self.guardspath)
717 717 self.guardsdirty = False
718 718 if self.added:
719 719 qrepo = self.qrepo()
720 720 if qrepo:
721 721 qrepo[None].add(f for f in self.added if f not in qrepo[None])
722 722 self.added = []
723 723
724 724 def removeundo(self, repo):
725 725 undo = repo.sjoin('undo')
726 726 if not os.path.exists(undo):
727 727 return
728 728 try:
729 729 os.unlink(undo)
730 730 except OSError as inst:
731 731 self.ui.warn(_('error removing undo: %s\n') %
732 732 stringutil.forcebytestr(inst))
733 733
734 734 def backup(self, repo, files, copy=False):
735 735 # backup local changes in --force case
736 736 for f in sorted(files):
737 737 absf = repo.wjoin(f)
738 738 if os.path.lexists(absf):
739 739 self.ui.note(_('saving current version of %s as %s\n') %
740 740 (f, scmutil.origpath(self.ui, repo, f)))
741 741
742 742 absorig = scmutil.origpath(self.ui, repo, absf)
743 743 if copy:
744 744 util.copyfile(absf, absorig)
745 745 else:
746 746 util.rename(absf, absorig)
747 747
748 748 def printdiff(self, repo, diffopts, node1, node2=None, files=None,
749 749 fp=None, changes=None, opts=None):
750 750 if opts is None:
751 751 opts = {}
752 752 stat = opts.get('stat')
753 753 m = scmutil.match(repo[node1], files, opts)
754 754 logcmdutil.diffordiffstat(self.ui, repo, diffopts, node1, node2, m,
755 755 changes, stat, fp)
756 756
757 757 def mergeone(self, repo, mergeq, head, patch, rev, diffopts):
758 758 # first try just applying the patch
759 759 (err, n) = self.apply(repo, [patch], update_status=False,
760 760 strict=True, merge=rev)
761 761
762 762 if err == 0:
763 763 return (err, n)
764 764
765 765 if n is None:
766 766 raise error.Abort(_("apply failed for patch %s") % patch)
767 767
768 768 self.ui.warn(_("patch didn't work out, merging %s\n") % patch)
769 769
770 770 # apply failed, strip away that rev and merge.
771 771 hg.clean(repo, head)
772 772 strip(self.ui, repo, [n], update=False, backup=False)
773 773
774 774 ctx = repo[rev]
775 775 ret = hg.merge(repo, rev)
776 776 if ret:
777 777 raise error.Abort(_("update returned %d") % ret)
778 778 n = newcommit(repo, None, ctx.description(), ctx.user(), force=True)
779 779 if n is None:
780 780 raise error.Abort(_("repo commit failed"))
781 781 try:
782 782 ph = patchheader(mergeq.join(patch), self.plainmode)
783 783 except Exception:
784 784 raise error.Abort(_("unable to read %s") % patch)
785 785
786 786 diffopts = self.patchopts(diffopts, patch)
787 787 patchf = self.opener(patch, "w")
788 788 comments = bytes(ph)
789 789 if comments:
790 790 patchf.write(comments)
791 791 self.printdiff(repo, diffopts, head, n, fp=patchf)
792 792 patchf.close()
793 793 self.removeundo(repo)
794 794 return (0, n)
795 795
796 796 def qparents(self, repo, rev=None):
797 797 """return the mq handled parent or p1
798 798
799 799 In some case where mq get himself in being the parent of a merge the
800 800 appropriate parent may be p2.
801 801 (eg: an in progress merge started with mq disabled)
802 802
803 803 If no parent are managed by mq, p1 is returned.
804 804 """
805 805 if rev is None:
806 806 (p1, p2) = repo.dirstate.parents()
807 807 if p2 == nullid:
808 808 return p1
809 809 if not self.applied:
810 810 return None
811 811 return self.applied[-1].node
812 812 p1, p2 = repo.changelog.parents(rev)
813 813 if p2 != nullid and p2 in [x.node for x in self.applied]:
814 814 return p2
815 815 return p1
816 816
817 817 def mergepatch(self, repo, mergeq, series, diffopts):
818 818 if not self.applied:
819 819 # each of the patches merged in will have two parents. This
820 820 # can confuse the qrefresh, qdiff, and strip code because it
821 821 # needs to know which parent is actually in the patch queue.
822 822 # so, we insert a merge marker with only one parent. This way
823 823 # the first patch in the queue is never a merge patch
824 824 #
825 825 pname = ".hg.patches.merge.marker"
826 826 n = newcommit(repo, None, '[mq]: merge marker', force=True)
827 827 self.removeundo(repo)
828 828 self.applied.append(statusentry(n, pname))
829 829 self.applieddirty = True
830 830
831 831 head = self.qparents(repo)
832 832
833 833 for patch in series:
834 834 patch = mergeq.lookup(patch, strict=True)
835 835 if not patch:
836 836 self.ui.warn(_("patch %s does not exist\n") % patch)
837 837 return (1, None)
838 838 pushable, reason = self.pushable(patch)
839 839 if not pushable:
840 840 self.explainpushable(patch, all_patches=True)
841 841 continue
842 842 info = mergeq.isapplied(patch)
843 843 if not info:
844 844 self.ui.warn(_("patch %s is not applied\n") % patch)
845 845 return (1, None)
846 846 rev = info[1]
847 847 err, head = self.mergeone(repo, mergeq, head, patch, rev, diffopts)
848 848 if head:
849 849 self.applied.append(statusentry(head, patch))
850 850 self.applieddirty = True
851 851 if err:
852 852 return (err, head)
853 853 self.savedirty()
854 854 return (0, head)
855 855
856 856 def patch(self, repo, patchfile):
857 857 '''Apply patchfile to the working directory.
858 858 patchfile: name of patch file'''
859 859 files = set()
860 860 try:
861 861 fuzz = patchmod.patch(self.ui, repo, patchfile, strip=1,
862 862 files=files, eolmode=None)
863 863 return (True, list(files), fuzz)
864 864 except Exception as inst:
865 865 self.ui.note(stringutil.forcebytestr(inst) + '\n')
866 866 if not self.ui.verbose:
867 867 self.ui.warn(_("patch failed, unable to continue (try -v)\n"))
868 868 self.ui.traceback()
869 869 return (False, list(files), False)
870 870
871 871 def apply(self, repo, series, list=False, update_status=True,
872 872 strict=False, patchdir=None, merge=None, all_files=None,
873 873 tobackup=None, keepchanges=False):
874 874 wlock = lock = tr = None
875 875 try:
876 876 wlock = repo.wlock()
877 877 lock = repo.lock()
878 878 tr = repo.transaction("qpush")
879 879 try:
880 880 ret = self._apply(repo, series, list, update_status,
881 881 strict, patchdir, merge, all_files=all_files,
882 882 tobackup=tobackup, keepchanges=keepchanges)
883 883 tr.close()
884 884 self.savedirty()
885 885 return ret
886 886 except AbortNoCleanup:
887 887 tr.close()
888 888 self.savedirty()
889 889 raise
890 890 except: # re-raises
891 891 try:
892 892 tr.abort()
893 893 finally:
894 894 self.invalidate()
895 895 raise
896 896 finally:
897 897 release(tr, lock, wlock)
898 898 self.removeundo(repo)
899 899
900 900 def _apply(self, repo, series, list=False, update_status=True,
901 901 strict=False, patchdir=None, merge=None, all_files=None,
902 902 tobackup=None, keepchanges=False):
903 903 """returns (error, hash)
904 904
905 905 error = 1 for unable to read, 2 for patch failed, 3 for patch
906 906 fuzz. tobackup is None or a set of files to backup before they
907 907 are modified by a patch.
908 908 """
909 909 # TODO unify with commands.py
910 910 if not patchdir:
911 911 patchdir = self.path
912 912 err = 0
913 913 n = None
914 914 for patchname in series:
915 915 pushable, reason = self.pushable(patchname)
916 916 if not pushable:
917 917 self.explainpushable(patchname, all_patches=True)
918 918 continue
919 919 self.ui.status(_("applying %s\n") % patchname)
920 920 pf = os.path.join(patchdir, patchname)
921 921
922 922 try:
923 923 ph = patchheader(self.join(patchname), self.plainmode)
924 924 except IOError:
925 925 self.ui.warn(_("unable to read %s\n") % patchname)
926 926 err = 1
927 927 break
928 928
929 929 message = ph.message
930 930 if not message:
931 931 # The commit message should not be translated
932 932 message = "imported patch %s\n" % patchname
933 933 else:
934 934 if list:
935 935 # The commit message should not be translated
936 936 message.append("\nimported patch %s" % patchname)
937 937 message = '\n'.join(message)
938 938
939 939 if ph.haspatch:
940 940 if tobackup:
941 941 touched = patchmod.changedfiles(self.ui, repo, pf)
942 942 touched = set(touched) & tobackup
943 943 if touched and keepchanges:
944 944 raise AbortNoCleanup(
945 945 _("conflicting local changes found"),
946 946 hint=_("did you forget to qrefresh?"))
947 947 self.backup(repo, touched, copy=True)
948 948 tobackup = tobackup - touched
949 949 (patcherr, files, fuzz) = self.patch(repo, pf)
950 950 if all_files is not None:
951 951 all_files.update(files)
952 952 patcherr = not patcherr
953 953 else:
954 954 self.ui.warn(_("patch %s is empty\n") % patchname)
955 955 patcherr, files, fuzz = 0, [], 0
956 956
957 957 if merge and files:
958 958 # Mark as removed/merged and update dirstate parent info
959 959 removed = []
960 960 merged = []
961 961 for f in files:
962 962 if os.path.lexists(repo.wjoin(f)):
963 963 merged.append(f)
964 964 else:
965 965 removed.append(f)
966 966 with repo.dirstate.parentchange():
967 967 for f in removed:
968 968 repo.dirstate.remove(f)
969 969 for f in merged:
970 970 repo.dirstate.merge(f)
971 971 p1, p2 = repo.dirstate.parents()
972 972 repo.setparents(p1, merge)
973 973
974 974 if all_files and '.hgsubstate' in all_files:
975 975 wctx = repo[None]
976 976 pctx = repo['.']
977 977 overwrite = False
978 978 mergedsubstate = subrepoutil.submerge(repo, pctx, wctx, wctx,
979 979 overwrite)
980 980 files += mergedsubstate.keys()
981 981
982 982 match = scmutil.matchfiles(repo, files or [])
983 983 oldtip = repo.changelog.tip()
984 984 n = newcommit(repo, None, message, ph.user, ph.date, match=match,
985 985 force=True)
986 986 if repo.changelog.tip() == oldtip:
987 987 raise error.Abort(_("qpush exactly duplicates child changeset"))
988 988 if n is None:
989 989 raise error.Abort(_("repository commit failed"))
990 990
991 991 if update_status:
992 992 self.applied.append(statusentry(n, patchname))
993 993
994 994 if patcherr:
995 995 self.ui.warn(_("patch failed, rejects left in working "
996 996 "directory\n"))
997 997 err = 2
998 998 break
999 999
1000 1000 if fuzz and strict:
1001 1001 self.ui.warn(_("fuzz found when applying patch, stopping\n"))
1002 1002 err = 3
1003 1003 break
1004 1004 return (err, n)
1005 1005
1006 1006 def _cleanup(self, patches, numrevs, keep=False):
1007 1007 if not keep:
1008 1008 r = self.qrepo()
1009 1009 if r:
1010 1010 r[None].forget(patches)
1011 1011 for p in patches:
1012 1012 try:
1013 1013 os.unlink(self.join(p))
1014 1014 except OSError as inst:
1015 1015 if inst.errno != errno.ENOENT:
1016 1016 raise
1017 1017
1018 1018 qfinished = []
1019 1019 if numrevs:
1020 1020 qfinished = self.applied[:numrevs]
1021 1021 del self.applied[:numrevs]
1022 1022 self.applieddirty = True
1023 1023
1024 1024 unknown = []
1025 1025
1026 1026 sortedseries = []
1027 1027 for p in patches:
1028 1028 idx = self.findseries(p)
1029 1029 if idx is None:
1030 1030 sortedseries.append((-1, p))
1031 1031 else:
1032 1032 sortedseries.append((idx, p))
1033 1033
1034 1034 sortedseries.sort(reverse=True)
1035 1035 for (i, p) in sortedseries:
1036 1036 if i != -1:
1037 1037 del self.fullseries[i]
1038 1038 else:
1039 1039 unknown.append(p)
1040 1040
1041 1041 if unknown:
1042 1042 if numrevs:
1043 1043 rev = dict((entry.name, entry.node) for entry in qfinished)
1044 1044 for p in unknown:
1045 1045 msg = _('revision %s refers to unknown patches: %s\n')
1046 1046 self.ui.warn(msg % (short(rev[p]), p))
1047 1047 else:
1048 1048 msg = _('unknown patches: %s\n')
1049 1049 raise error.Abort(''.join(msg % p for p in unknown))
1050 1050
1051 1051 self.parseseries()
1052 1052 self.seriesdirty = True
1053 1053 return [entry.node for entry in qfinished]
1054 1054
1055 1055 def _revpatches(self, repo, revs):
1056 1056 firstrev = repo[self.applied[0].node].rev()
1057 1057 patches = []
1058 1058 for i, rev in enumerate(revs):
1059 1059
1060 1060 if rev < firstrev:
1061 1061 raise error.Abort(_('revision %d is not managed') % rev)
1062 1062
1063 1063 ctx = repo[rev]
1064 1064 base = self.applied[i].node
1065 1065 if ctx.node() != base:
1066 1066 msg = _('cannot delete revision %d above applied patches')
1067 1067 raise error.Abort(msg % rev)
1068 1068
1069 1069 patch = self.applied[i].name
1070 1070 for fmt in ('[mq]: %s', 'imported patch %s'):
1071 1071 if ctx.description() == fmt % patch:
1072 1072 msg = _('patch %s finalized without changeset message\n')
1073 1073 repo.ui.status(msg % patch)
1074 1074 break
1075 1075
1076 1076 patches.append(patch)
1077 1077 return patches
1078 1078
1079 1079 def finish(self, repo, revs):
1080 1080 # Manually trigger phase computation to ensure phasedefaults is
1081 1081 # executed before we remove the patches.
1082 1082 repo._phasecache
1083 1083 patches = self._revpatches(repo, sorted(revs))
1084 1084 qfinished = self._cleanup(patches, len(patches))
1085 1085 if qfinished and repo.ui.configbool('mq', 'secret'):
1086 1086 # only use this logic when the secret option is added
1087 1087 oldqbase = repo[qfinished[0]]
1088 1088 tphase = phases.newcommitphase(repo.ui)
1089 1089 if oldqbase.phase() > tphase and oldqbase.p1().phase() <= tphase:
1090 1090 with repo.transaction('qfinish') as tr:
1091 1091 phases.advanceboundary(repo, tr, tphase, qfinished)
1092 1092
1093 1093 def delete(self, repo, patches, opts):
1094 1094 if not patches and not opts.get('rev'):
1095 1095 raise error.Abort(_('qdelete requires at least one revision or '
1096 1096 'patch name'))
1097 1097
1098 1098 realpatches = []
1099 1099 for patch in patches:
1100 1100 patch = self.lookup(patch, strict=True)
1101 1101 info = self.isapplied(patch)
1102 1102 if info:
1103 1103 raise error.Abort(_("cannot delete applied patch %s") % patch)
1104 1104 if patch not in self.series:
1105 1105 raise error.Abort(_("patch %s not in series file") % patch)
1106 1106 if patch not in realpatches:
1107 1107 realpatches.append(patch)
1108 1108
1109 1109 numrevs = 0
1110 1110 if opts.get('rev'):
1111 1111 if not self.applied:
1112 1112 raise error.Abort(_('no patches applied'))
1113 1113 revs = scmutil.revrange(repo, opts.get('rev'))
1114 1114 revs.sort()
1115 1115 revpatches = self._revpatches(repo, revs)
1116 1116 realpatches += revpatches
1117 1117 numrevs = len(revpatches)
1118 1118
1119 1119 self._cleanup(realpatches, numrevs, opts.get('keep'))
1120 1120
1121 1121 def checktoppatch(self, repo):
1122 1122 '''check that working directory is at qtip'''
1123 1123 if self.applied:
1124 1124 top = self.applied[-1].node
1125 1125 patch = self.applied[-1].name
1126 1126 if repo.dirstate.p1() != top:
1127 1127 raise error.Abort(_("working directory revision is not qtip"))
1128 1128 return top, patch
1129 1129 return None, None
1130 1130
1131 1131 def putsubstate2changes(self, substatestate, changes):
1132 1132 for files in changes[:3]:
1133 1133 if '.hgsubstate' in files:
1134 1134 return # already listed up
1135 1135 # not yet listed up
1136 1136 if substatestate in 'a?':
1137 1137 changes[1].append('.hgsubstate')
1138 1138 elif substatestate in 'r':
1139 1139 changes[2].append('.hgsubstate')
1140 1140 else: # modified
1141 1141 changes[0].append('.hgsubstate')
1142 1142
1143 1143 def checklocalchanges(self, repo, force=False, refresh=True):
1144 1144 excsuffix = ''
1145 1145 if refresh:
1146 1146 excsuffix = ', qrefresh first'
1147 1147 # plain versions for i18n tool to detect them
1148 1148 _("local changes found, qrefresh first")
1149 1149 _("local changed subrepos found, qrefresh first")
1150 1150 return checklocalchanges(repo, force, excsuffix)
1151 1151
1152 1152 _reserved = ('series', 'status', 'guards', '.', '..')
1153 1153 def checkreservedname(self, name):
1154 1154 if name in self._reserved:
1155 1155 raise error.Abort(_('"%s" cannot be used as the name of a patch')
1156 1156 % name)
1157 1157 if name != name.strip():
1158 1158 # whitespace is stripped by parseseries()
1159 1159 raise error.Abort(_('patch name cannot begin or end with '
1160 1160 'whitespace'))
1161 1161 for prefix in ('.hg', '.mq'):
1162 1162 if name.startswith(prefix):
1163 1163 raise error.Abort(_('patch name cannot begin with "%s"')
1164 1164 % prefix)
1165 1165 for c in ('#', ':', '\r', '\n'):
1166 1166 if c in name:
1167 1167 raise error.Abort(_('%r cannot be used in the name of a patch')
1168 1168 % pycompat.bytestr(c))
1169 1169
1170 1170 def checkpatchname(self, name, force=False):
1171 1171 self.checkreservedname(name)
1172 1172 if not force and os.path.exists(self.join(name)):
1173 1173 if os.path.isdir(self.join(name)):
1174 1174 raise error.Abort(_('"%s" already exists as a directory')
1175 1175 % name)
1176 1176 else:
1177 1177 raise error.Abort(_('patch "%s" already exists') % name)
1178 1178
1179 1179 def makepatchname(self, title, fallbackname):
1180 1180 """Return a suitable filename for title, adding a suffix to make
1181 1181 it unique in the existing list"""
1182 1182 namebase = re.sub('[\s\W_]+', '_', title.lower()).strip('_')
1183 1183 namebase = namebase[:75] # avoid too long name (issue5117)
1184 1184 if namebase:
1185 1185 try:
1186 1186 self.checkreservedname(namebase)
1187 1187 except error.Abort:
1188 1188 namebase = fallbackname
1189 1189 else:
1190 1190 namebase = fallbackname
1191 1191 name = namebase
1192 1192 i = 0
1193 1193 while True:
1194 1194 if name not in self.fullseries:
1195 1195 try:
1196 1196 self.checkpatchname(name)
1197 1197 break
1198 1198 except error.Abort:
1199 1199 pass
1200 1200 i += 1
1201 1201 name = '%s__%d' % (namebase, i)
1202 1202 return name
1203 1203
1204 1204 def checkkeepchanges(self, keepchanges, force):
1205 1205 if force and keepchanges:
1206 1206 raise error.Abort(_('cannot use both --force and --keep-changes'))
1207 1207
1208 1208 def new(self, repo, patchfn, *pats, **opts):
1209 1209 """options:
1210 1210 msg: a string or a no-argument function returning a string
1211 1211 """
1212 1212 opts = pycompat.byteskwargs(opts)
1213 1213 msg = opts.get('msg')
1214 1214 edit = opts.get('edit')
1215 1215 editform = opts.get('editform', 'mq.qnew')
1216 1216 user = opts.get('user')
1217 1217 date = opts.get('date')
1218 1218 if date:
1219 1219 date = dateutil.parsedate(date)
1220 1220 diffopts = self.diffopts({'git': opts.get('git')}, plain=True)
1221 1221 if opts.get('checkname', True):
1222 1222 self.checkpatchname(patchfn)
1223 1223 inclsubs = checksubstate(repo)
1224 1224 if inclsubs:
1225 1225 substatestate = repo.dirstate['.hgsubstate']
1226 1226 if opts.get('include') or opts.get('exclude') or pats:
1227 1227 # detect missing files in pats
1228 1228 def badfn(f, msg):
1229 1229 if f != '.hgsubstate': # .hgsubstate is auto-created
1230 1230 raise error.Abort('%s: %s' % (f, msg))
1231 1231 match = scmutil.match(repo[None], pats, opts, badfn=badfn)
1232 1232 changes = repo.status(match=match)
1233 1233 else:
1234 1234 changes = self.checklocalchanges(repo, force=True)
1235 1235 commitfiles = list(inclsubs)
1236 1236 for files in changes[:3]:
1237 1237 commitfiles.extend(files)
1238 1238 match = scmutil.matchfiles(repo, commitfiles)
1239 1239 if len(repo[None].parents()) > 1:
1240 1240 raise error.Abort(_('cannot manage merge changesets'))
1241 1241 self.checktoppatch(repo)
1242 1242 insert = self.fullseriesend()
1243 1243 with repo.wlock():
1244 1244 try:
1245 1245 # if patch file write fails, abort early
1246 1246 p = self.opener(patchfn, "w")
1247 1247 except IOError as e:
1248 1248 raise error.Abort(_('cannot write patch "%s": %s')
1249 1249 % (patchfn, encoding.strtolocal(e.strerror)))
1250 1250 try:
1251 1251 defaultmsg = "[mq]: %s" % patchfn
1252 1252 editor = cmdutil.getcommiteditor(editform=editform)
1253 1253 if edit:
1254 1254 def finishdesc(desc):
1255 1255 if desc.rstrip():
1256 1256 return desc
1257 1257 else:
1258 1258 return defaultmsg
1259 1259 # i18n: this message is shown in editor with "HG: " prefix
1260 1260 extramsg = _('Leave message empty to use default message.')
1261 1261 editor = cmdutil.getcommiteditor(finishdesc=finishdesc,
1262 1262 extramsg=extramsg,
1263 1263 editform=editform)
1264 1264 commitmsg = msg
1265 1265 else:
1266 1266 commitmsg = msg or defaultmsg
1267 1267
1268 1268 n = newcommit(repo, None, commitmsg, user, date, match=match,
1269 1269 force=True, editor=editor)
1270 1270 if n is None:
1271 1271 raise error.Abort(_("repo commit failed"))
1272 1272 try:
1273 1273 self.fullseries[insert:insert] = [patchfn]
1274 1274 self.applied.append(statusentry(n, patchfn))
1275 1275 self.parseseries()
1276 1276 self.seriesdirty = True
1277 1277 self.applieddirty = True
1278 1278 nctx = repo[n]
1279 1279 ph = patchheader(self.join(patchfn), self.plainmode)
1280 1280 if user:
1281 1281 ph.setuser(user)
1282 1282 if date:
1283 1283 ph.setdate('%d %d' % date)
1284 1284 ph.setparent(hex(nctx.p1().node()))
1285 1285 msg = nctx.description().strip()
1286 1286 if msg == defaultmsg.strip():
1287 1287 msg = ''
1288 1288 ph.setmessage(msg)
1289 1289 p.write(bytes(ph))
1290 1290 if commitfiles:
1291 1291 parent = self.qparents(repo, n)
1292 1292 if inclsubs:
1293 1293 self.putsubstate2changes(substatestate, changes)
1294 1294 chunks = patchmod.diff(repo, node1=parent, node2=n,
1295 1295 changes=changes, opts=diffopts)
1296 1296 for chunk in chunks:
1297 1297 p.write(chunk)
1298 1298 p.close()
1299 1299 r = self.qrepo()
1300 1300 if r:
1301 1301 r[None].add([patchfn])
1302 1302 except: # re-raises
1303 1303 repo.rollback()
1304 1304 raise
1305 1305 except Exception:
1306 1306 patchpath = self.join(patchfn)
1307 1307 try:
1308 1308 os.unlink(patchpath)
1309 1309 except OSError:
1310 1310 self.ui.warn(_('error unlinking %s\n') % patchpath)
1311 1311 raise
1312 1312 self.removeundo(repo)
1313 1313
1314 1314 def isapplied(self, patch):
1315 1315 """returns (index, rev, patch)"""
1316 1316 for i, a in enumerate(self.applied):
1317 1317 if a.name == patch:
1318 1318 return (i, a.node, a.name)
1319 1319 return None
1320 1320
1321 1321 # if the exact patch name does not exist, we try a few
1322 1322 # variations. If strict is passed, we try only #1
1323 1323 #
1324 1324 # 1) a number (as string) to indicate an offset in the series file
1325 1325 # 2) a unique substring of the patch name was given
1326 1326 # 3) patchname[-+]num to indicate an offset in the series file
1327 1327 def lookup(self, patch, strict=False):
1328 1328 def partialname(s):
1329 1329 if s in self.series:
1330 1330 return s
1331 1331 matches = [x for x in self.series if s in x]
1332 1332 if len(matches) > 1:
1333 1333 self.ui.warn(_('patch name "%s" is ambiguous:\n') % s)
1334 1334 for m in matches:
1335 1335 self.ui.warn(' %s\n' % m)
1336 1336 return None
1337 1337 if matches:
1338 1338 return matches[0]
1339 1339 if self.series and self.applied:
1340 1340 if s == 'qtip':
1341 1341 return self.series[self.seriesend(True) - 1]
1342 1342 if s == 'qbase':
1343 1343 return self.series[0]
1344 1344 return None
1345 1345
1346 1346 if patch in self.series:
1347 1347 return patch
1348 1348
1349 1349 if not os.path.isfile(self.join(patch)):
1350 1350 try:
1351 1351 sno = int(patch)
1352 1352 except (ValueError, OverflowError):
1353 1353 pass
1354 1354 else:
1355 1355 if -len(self.series) <= sno < len(self.series):
1356 1356 return self.series[sno]
1357 1357
1358 1358 if not strict:
1359 1359 res = partialname(patch)
1360 1360 if res:
1361 1361 return res
1362 1362 minus = patch.rfind('-')
1363 1363 if minus >= 0:
1364 1364 res = partialname(patch[:minus])
1365 1365 if res:
1366 1366 i = self.series.index(res)
1367 1367 try:
1368 1368 off = int(patch[minus + 1:] or 1)
1369 1369 except (ValueError, OverflowError):
1370 1370 pass
1371 1371 else:
1372 1372 if i - off >= 0:
1373 1373 return self.series[i - off]
1374 1374 plus = patch.rfind('+')
1375 1375 if plus >= 0:
1376 1376 res = partialname(patch[:plus])
1377 1377 if res:
1378 1378 i = self.series.index(res)
1379 1379 try:
1380 1380 off = int(patch[plus + 1:] or 1)
1381 1381 except (ValueError, OverflowError):
1382 1382 pass
1383 1383 else:
1384 1384 if i + off < len(self.series):
1385 1385 return self.series[i + off]
1386 1386 raise error.Abort(_("patch %s not in series") % patch)
1387 1387
1388 1388 def push(self, repo, patch=None, force=False, list=False, mergeq=None,
1389 1389 all=False, move=False, exact=False, nobackup=False,
1390 1390 keepchanges=False):
1391 1391 self.checkkeepchanges(keepchanges, force)
1392 1392 diffopts = self.diffopts()
1393 1393 with repo.wlock():
1394 1394 heads = []
1395 1395 for hs in repo.branchmap().itervalues():
1396 1396 heads.extend(hs)
1397 1397 if not heads:
1398 1398 heads = [nullid]
1399 1399 if repo.dirstate.p1() not in heads and not exact:
1400 1400 self.ui.status(_("(working directory not at a head)\n"))
1401 1401
1402 1402 if not self.series:
1403 1403 self.ui.warn(_('no patches in series\n'))
1404 1404 return 0
1405 1405
1406 1406 # Suppose our series file is: A B C and the current 'top'
1407 1407 # patch is B. qpush C should be performed (moving forward)
1408 1408 # qpush B is a NOP (no change) qpush A is an error (can't
1409 1409 # go backwards with qpush)
1410 1410 if patch:
1411 1411 patch = self.lookup(patch)
1412 1412 info = self.isapplied(patch)
1413 1413 if info and info[0] >= len(self.applied) - 1:
1414 1414 self.ui.warn(
1415 1415 _('qpush: %s is already at the top\n') % patch)
1416 1416 return 0
1417 1417
1418 1418 pushable, reason = self.pushable(patch)
1419 1419 if pushable:
1420 1420 if self.series.index(patch) < self.seriesend():
1421 1421 raise error.Abort(
1422 1422 _("cannot push to a previous patch: %s") % patch)
1423 1423 else:
1424 1424 if reason:
1425 1425 reason = _('guarded by %s') % reason
1426 1426 else:
1427 1427 reason = _('no matching guards')
1428 1428 self.ui.warn(_("cannot push '%s' - %s\n") % (patch, reason))
1429 1429 return 1
1430 1430 elif all:
1431 1431 patch = self.series[-1]
1432 1432 if self.isapplied(patch):
1433 1433 self.ui.warn(_('all patches are currently applied\n'))
1434 1434 return 0
1435 1435
1436 1436 # Following the above example, starting at 'top' of B:
1437 1437 # qpush should be performed (pushes C), but a subsequent
1438 1438 # qpush without an argument is an error (nothing to
1439 1439 # apply). This allows a loop of "...while hg qpush..." to
1440 1440 # work as it detects an error when done
1441 1441 start = self.seriesend()
1442 1442 if start == len(self.series):
1443 1443 self.ui.warn(_('patch series already fully applied\n'))
1444 1444 return 1
1445 1445 if not force and not keepchanges:
1446 1446 self.checklocalchanges(repo, refresh=self.applied)
1447 1447
1448 1448 if exact:
1449 1449 if keepchanges:
1450 1450 raise error.Abort(
1451 1451 _("cannot use --exact and --keep-changes together"))
1452 1452 if move:
1453 1453 raise error.Abort(_('cannot use --exact and --move '
1454 1454 'together'))
1455 1455 if self.applied:
1456 1456 raise error.Abort(_('cannot push --exact with applied '
1457 1457 'patches'))
1458 1458 root = self.series[start]
1459 1459 target = patchheader(self.join(root), self.plainmode).parent
1460 1460 if not target:
1461 1461 raise error.Abort(
1462 1462 _("%s does not have a parent recorded") % root)
1463 1463 if not repo[target] == repo['.']:
1464 1464 hg.update(repo, target)
1465 1465
1466 1466 if move:
1467 1467 if not patch:
1468 1468 raise error.Abort(_("please specify the patch to move"))
1469 1469 for fullstart, rpn in enumerate(self.fullseries):
1470 1470 # strip markers for patch guards
1471 1471 if self.guard_re.split(rpn, 1)[0] == self.series[start]:
1472 1472 break
1473 1473 for i, rpn in enumerate(self.fullseries[fullstart:]):
1474 1474 # strip markers for patch guards
1475 1475 if self.guard_re.split(rpn, 1)[0] == patch:
1476 1476 break
1477 1477 index = fullstart + i
1478 1478 assert index < len(self.fullseries)
1479 1479 fullpatch = self.fullseries[index]
1480 1480 del self.fullseries[index]
1481 1481 self.fullseries.insert(fullstart, fullpatch)
1482 1482 self.parseseries()
1483 1483 self.seriesdirty = True
1484 1484
1485 1485 self.applieddirty = True
1486 1486 if start > 0:
1487 1487 self.checktoppatch(repo)
1488 1488 if not patch:
1489 1489 patch = self.series[start]
1490 1490 end = start + 1
1491 1491 else:
1492 1492 end = self.series.index(patch, start) + 1
1493 1493
1494 1494 tobackup = set()
1495 1495 if (not nobackup and force) or keepchanges:
1496 1496 status = self.checklocalchanges(repo, force=True)
1497 1497 if keepchanges:
1498 1498 tobackup.update(status.modified + status.added +
1499 1499 status.removed + status.deleted)
1500 1500 else:
1501 1501 tobackup.update(status.modified + status.added)
1502 1502
1503 1503 s = self.series[start:end]
1504 1504 all_files = set()
1505 1505 try:
1506 1506 if mergeq:
1507 1507 ret = self.mergepatch(repo, mergeq, s, diffopts)
1508 1508 else:
1509 1509 ret = self.apply(repo, s, list, all_files=all_files,
1510 1510 tobackup=tobackup, keepchanges=keepchanges)
1511 1511 except AbortNoCleanup:
1512 1512 raise
1513 1513 except: # re-raises
1514 1514 self.ui.warn(_('cleaning up working directory...\n'))
1515 1515 cmdutil.revert(self.ui, repo, repo['.'],
1516 1516 repo.dirstate.parents(), no_backup=True)
1517 1517 # only remove unknown files that we know we touched or
1518 1518 # created while patching
1519 1519 for f in all_files:
1520 1520 if f not in repo.dirstate:
1521 1521 repo.wvfs.unlinkpath(f, ignoremissing=True)
1522 1522 self.ui.warn(_('done\n'))
1523 1523 raise
1524 1524
1525 1525 if not self.applied:
1526 1526 return ret[0]
1527 1527 top = self.applied[-1].name
1528 1528 if ret[0] and ret[0] > 1:
1529 1529 msg = _("errors during apply, please fix and qrefresh %s\n")
1530 1530 self.ui.write(msg % top)
1531 1531 else:
1532 1532 self.ui.write(_("now at: %s\n") % top)
1533 1533 return ret[0]
1534 1534
1535 1535 def pop(self, repo, patch=None, force=False, update=True, all=False,
1536 1536 nobackup=False, keepchanges=False):
1537 1537 self.checkkeepchanges(keepchanges, force)
1538 1538 with repo.wlock():
1539 1539 if patch:
1540 1540 # index, rev, patch
1541 1541 info = self.isapplied(patch)
1542 1542 if not info:
1543 1543 patch = self.lookup(patch)
1544 1544 info = self.isapplied(patch)
1545 1545 if not info:
1546 1546 raise error.Abort(_("patch %s is not applied") % patch)
1547 1547
1548 1548 if not self.applied:
1549 1549 # Allow qpop -a to work repeatedly,
1550 1550 # but not qpop without an argument
1551 1551 self.ui.warn(_("no patches applied\n"))
1552 1552 return not all
1553 1553
1554 1554 if all:
1555 1555 start = 0
1556 1556 elif patch:
1557 1557 start = info[0] + 1
1558 1558 else:
1559 1559 start = len(self.applied) - 1
1560 1560
1561 1561 if start >= len(self.applied):
1562 1562 self.ui.warn(_("qpop: %s is already at the top\n") % patch)
1563 1563 return
1564 1564
1565 1565 if not update:
1566 1566 parents = repo.dirstate.parents()
1567 1567 rr = [x.node for x in self.applied]
1568 1568 for p in parents:
1569 1569 if p in rr:
1570 1570 self.ui.warn(_("qpop: forcing dirstate update\n"))
1571 1571 update = True
1572 1572 else:
1573 1573 parents = [p.node() for p in repo[None].parents()]
1574 1574 update = any(entry.node in parents
1575 1575 for entry in self.applied[start:])
1576 1576
1577 1577 tobackup = set()
1578 1578 if update:
1579 1579 s = self.checklocalchanges(repo, force=force or keepchanges)
1580 1580 if force:
1581 1581 if not nobackup:
1582 1582 tobackup.update(s.modified + s.added)
1583 1583 elif keepchanges:
1584 1584 tobackup.update(s.modified + s.added +
1585 1585 s.removed + s.deleted)
1586 1586
1587 1587 self.applieddirty = True
1588 1588 end = len(self.applied)
1589 1589 rev = self.applied[start].node
1590 1590
1591 1591 try:
1592 1592 heads = repo.changelog.heads(rev)
1593 1593 except error.LookupError:
1594 1594 node = short(rev)
1595 1595 raise error.Abort(_('trying to pop unknown node %s') % node)
1596 1596
1597 1597 if heads != [self.applied[-1].node]:
1598 1598 raise error.Abort(_("popping would remove a revision not "
1599 1599 "managed by this patch queue"))
1600 1600 if not repo[self.applied[-1].node].mutable():
1601 1601 raise error.Abort(
1602 1602 _("popping would remove a public revision"),
1603 1603 hint=_("see 'hg help phases' for details"))
1604 1604
1605 1605 # we know there are no local changes, so we can make a simplified
1606 1606 # form of hg.update.
1607 1607 if update:
1608 1608 qp = self.qparents(repo, rev)
1609 1609 ctx = repo[qp]
1610 1610 m, a, r, d = repo.status(qp, '.')[:4]
1611 1611 if d:
1612 1612 raise error.Abort(_("deletions found between repo revs"))
1613 1613
1614 1614 tobackup = set(a + m + r) & tobackup
1615 1615 if keepchanges and tobackup:
1616 1616 raise error.Abort(_("local changes found, qrefresh first"))
1617 1617 self.backup(repo, tobackup)
1618 1618 with repo.dirstate.parentchange():
1619 1619 for f in a:
1620 1620 repo.wvfs.unlinkpath(f, ignoremissing=True)
1621 1621 repo.dirstate.drop(f)
1622 1622 for f in m + r:
1623 1623 fctx = ctx[f]
1624 1624 repo.wwrite(f, fctx.data(), fctx.flags())
1625 1625 repo.dirstate.normal(f)
1626 1626 repo.setparents(qp, nullid)
1627 1627 for patch in reversed(self.applied[start:end]):
1628 1628 self.ui.status(_("popping %s\n") % patch.name)
1629 1629 del self.applied[start:end]
1630 1630 strip(self.ui, repo, [rev], update=False, backup=False)
1631 1631 for s, state in repo['.'].substate.items():
1632 1632 repo['.'].sub(s).get(state)
1633 1633 if self.applied:
1634 1634 self.ui.write(_("now at: %s\n") % self.applied[-1].name)
1635 1635 else:
1636 1636 self.ui.write(_("patch queue now empty\n"))
1637 1637
1638 1638 def diff(self, repo, pats, opts):
1639 1639 top, patch = self.checktoppatch(repo)
1640 1640 if not top:
1641 1641 self.ui.write(_("no patches applied\n"))
1642 1642 return
1643 1643 qp = self.qparents(repo, top)
1644 1644 if opts.get('reverse'):
1645 1645 node1, node2 = None, qp
1646 1646 else:
1647 1647 node1, node2 = qp, None
1648 1648 diffopts = self.diffopts(opts, patch)
1649 1649 self.printdiff(repo, diffopts, node1, node2, files=pats, opts=opts)
1650 1650
1651 1651 def refresh(self, repo, pats=None, **opts):
1652 1652 opts = pycompat.byteskwargs(opts)
1653 1653 if not self.applied:
1654 1654 self.ui.write(_("no patches applied\n"))
1655 1655 return 1
1656 1656 msg = opts.get('msg', '').rstrip()
1657 1657 edit = opts.get('edit')
1658 1658 editform = opts.get('editform', 'mq.qrefresh')
1659 1659 newuser = opts.get('user')
1660 1660 newdate = opts.get('date')
1661 1661 if newdate:
1662 1662 newdate = '%d %d' % dateutil.parsedate(newdate)
1663 1663 wlock = repo.wlock()
1664 1664
1665 1665 try:
1666 1666 self.checktoppatch(repo)
1667 1667 (top, patchfn) = (self.applied[-1].node, self.applied[-1].name)
1668 1668 if repo.changelog.heads(top) != [top]:
1669 1669 raise error.Abort(_("cannot qrefresh a revision with children"))
1670 1670 if not repo[top].mutable():
1671 1671 raise error.Abort(_("cannot qrefresh public revision"),
1672 1672 hint=_("see 'hg help phases' for details"))
1673 1673
1674 1674 cparents = repo.changelog.parents(top)
1675 1675 patchparent = self.qparents(repo, top)
1676 1676
1677 1677 inclsubs = checksubstate(repo, patchparent)
1678 1678 if inclsubs:
1679 1679 substatestate = repo.dirstate['.hgsubstate']
1680 1680
1681 1681 ph = patchheader(self.join(patchfn), self.plainmode)
1682 1682 diffopts = self.diffopts({'git': opts.get('git')}, patchfn,
1683 1683 plain=True)
1684 1684 if newuser:
1685 1685 ph.setuser(newuser)
1686 1686 if newdate:
1687 1687 ph.setdate(newdate)
1688 1688 ph.setparent(hex(patchparent))
1689 1689
1690 1690 # only commit new patch when write is complete
1691 1691 patchf = self.opener(patchfn, 'w', atomictemp=True)
1692 1692
1693 1693 # update the dirstate in place, strip off the qtip commit
1694 1694 # and then commit.
1695 1695 #
1696 1696 # this should really read:
1697 1697 # mm, dd, aa = repo.status(top, patchparent)[:3]
1698 1698 # but we do it backwards to take advantage of manifest/changelog
1699 1699 # caching against the next repo.status call
1700 1700 mm, aa, dd = repo.status(patchparent, top)[:3]
1701 1701 changes = repo.changelog.read(top)
1702 1702 man = repo.manifestlog[changes[0]].read()
1703 1703 aaa = aa[:]
1704 1704 match1 = scmutil.match(repo[None], pats, opts)
1705 1705 # in short mode, we only diff the files included in the
1706 1706 # patch already plus specified files
1707 1707 if opts.get('short'):
1708 1708 # if amending a patch, we start with existing
1709 1709 # files plus specified files - unfiltered
1710 1710 match = scmutil.matchfiles(repo, mm + aa + dd + match1.files())
1711 1711 # filter with include/exclude options
1712 1712 match1 = scmutil.match(repo[None], opts=opts)
1713 1713 else:
1714 1714 match = scmutil.matchall(repo)
1715 1715 m, a, r, d = repo.status(match=match)[:4]
1716 1716 mm = set(mm)
1717 1717 aa = set(aa)
1718 1718 dd = set(dd)
1719 1719
1720 1720 # we might end up with files that were added between
1721 1721 # qtip and the dirstate parent, but then changed in the
1722 1722 # local dirstate. in this case, we want them to only
1723 1723 # show up in the added section
1724 1724 for x in m:
1725 1725 if x not in aa:
1726 1726 mm.add(x)
1727 1727 # we might end up with files added by the local dirstate that
1728 1728 # were deleted by the patch. In this case, they should only
1729 1729 # show up in the changed section.
1730 1730 for x in a:
1731 1731 if x in dd:
1732 1732 dd.remove(x)
1733 1733 mm.add(x)
1734 1734 else:
1735 1735 aa.add(x)
1736 1736 # make sure any files deleted in the local dirstate
1737 1737 # are not in the add or change column of the patch
1738 1738 forget = []
1739 1739 for x in d + r:
1740 1740 if x in aa:
1741 1741 aa.remove(x)
1742 1742 forget.append(x)
1743 1743 continue
1744 1744 else:
1745 1745 mm.discard(x)
1746 1746 dd.add(x)
1747 1747
1748 1748 m = list(mm)
1749 1749 r = list(dd)
1750 1750 a = list(aa)
1751 1751
1752 1752 # create 'match' that includes the files to be recommitted.
1753 1753 # apply match1 via repo.status to ensure correct case handling.
1754 1754 cm, ca, cr, cd = repo.status(patchparent, match=match1)[:4]
1755 1755 allmatches = set(cm + ca + cr + cd)
1756 1756 refreshchanges = [x.intersection(allmatches) for x in (mm, aa, dd)]
1757 1757
1758 1758 files = set(inclsubs)
1759 1759 for x in refreshchanges:
1760 1760 files.update(x)
1761 1761 match = scmutil.matchfiles(repo, files)
1762 1762
1763 1763 bmlist = repo[top].bookmarks()
1764 1764
1765 1765 dsguard = None
1766 1766 try:
1767 1767 dsguard = dirstateguard.dirstateguard(repo, 'mq.refresh')
1768 1768 if diffopts.git or diffopts.upgrade:
1769 1769 copies = {}
1770 1770 for dst in a:
1771 1771 src = repo.dirstate.copied(dst)
1772 1772 # during qfold, the source file for copies may
1773 1773 # be removed. Treat this as a simple add.
1774 1774 if src is not None and src in repo.dirstate:
1775 1775 copies.setdefault(src, []).append(dst)
1776 1776 repo.dirstate.add(dst)
1777 1777 # remember the copies between patchparent and qtip
1778 1778 for dst in aaa:
1779 1779 f = repo.file(dst)
1780 1780 src = f.renamed(man[dst])
1781 1781 if src:
1782 1782 copies.setdefault(src[0], []).extend(
1783 1783 copies.get(dst, []))
1784 1784 if dst in a:
1785 1785 copies[src[0]].append(dst)
1786 1786 # we can't copy a file created by the patch itself
1787 1787 if dst in copies:
1788 1788 del copies[dst]
1789 1789 for src, dsts in copies.iteritems():
1790 1790 for dst in dsts:
1791 1791 repo.dirstate.copy(src, dst)
1792 1792 else:
1793 1793 for dst in a:
1794 1794 repo.dirstate.add(dst)
1795 1795 # Drop useless copy information
1796 1796 for f in list(repo.dirstate.copies()):
1797 1797 repo.dirstate.copy(None, f)
1798 1798 for f in r:
1799 1799 repo.dirstate.remove(f)
1800 1800 # if the patch excludes a modified file, mark that
1801 1801 # file with mtime=0 so status can see it.
1802 1802 mm = []
1803 1803 for i in pycompat.xrange(len(m) - 1, -1, -1):
1804 1804 if not match1(m[i]):
1805 1805 mm.append(m[i])
1806 1806 del m[i]
1807 1807 for f in m:
1808 1808 repo.dirstate.normal(f)
1809 1809 for f in mm:
1810 1810 repo.dirstate.normallookup(f)
1811 1811 for f in forget:
1812 1812 repo.dirstate.drop(f)
1813 1813
1814 1814 user = ph.user or changes[1]
1815 1815
1816 1816 oldphase = repo[top].phase()
1817 1817
1818 1818 # assumes strip can roll itself back if interrupted
1819 1819 repo.setparents(*cparents)
1820 1820 self.applied.pop()
1821 1821 self.applieddirty = True
1822 1822 strip(self.ui, repo, [top], update=False, backup=False)
1823 1823 dsguard.close()
1824 1824 finally:
1825 1825 release(dsguard)
1826 1826
1827 1827 try:
1828 1828 # might be nice to attempt to roll back strip after this
1829 1829
1830 1830 defaultmsg = "[mq]: %s" % patchfn
1831 1831 editor = cmdutil.getcommiteditor(editform=editform)
1832 1832 if edit:
1833 1833 def finishdesc(desc):
1834 1834 if desc.rstrip():
1835 1835 ph.setmessage(desc)
1836 1836 return desc
1837 1837 return defaultmsg
1838 1838 # i18n: this message is shown in editor with "HG: " prefix
1839 1839 extramsg = _('Leave message empty to use default message.')
1840 1840 editor = cmdutil.getcommiteditor(finishdesc=finishdesc,
1841 1841 extramsg=extramsg,
1842 1842 editform=editform)
1843 1843 message = msg or "\n".join(ph.message)
1844 1844 elif not msg:
1845 1845 if not ph.message:
1846 1846 message = defaultmsg
1847 1847 else:
1848 1848 message = "\n".join(ph.message)
1849 1849 else:
1850 1850 message = msg
1851 1851 ph.setmessage(msg)
1852 1852
1853 1853 # Ensure we create a new changeset in the same phase than
1854 1854 # the old one.
1855 1855 lock = tr = None
1856 1856 try:
1857 1857 lock = repo.lock()
1858 1858 tr = repo.transaction('mq')
1859 1859 n = newcommit(repo, oldphase, message, user, ph.date,
1860 1860 match=match, force=True, editor=editor)
1861 1861 # only write patch after a successful commit
1862 1862 c = [list(x) for x in refreshchanges]
1863 1863 if inclsubs:
1864 1864 self.putsubstate2changes(substatestate, c)
1865 1865 chunks = patchmod.diff(repo, patchparent,
1866 1866 changes=c, opts=diffopts)
1867 1867 comments = bytes(ph)
1868 1868 if comments:
1869 1869 patchf.write(comments)
1870 1870 for chunk in chunks:
1871 1871 patchf.write(chunk)
1872 1872 patchf.close()
1873 1873
1874 1874 marks = repo._bookmarks
1875 1875 marks.applychanges(repo, tr, [(bm, n) for bm in bmlist])
1876 1876 tr.close()
1877 1877
1878 1878 self.applied.append(statusentry(n, patchfn))
1879 1879 finally:
1880 1880 lockmod.release(tr, lock)
1881 1881 except: # re-raises
1882 1882 ctx = repo[cparents[0]]
1883 1883 repo.dirstate.rebuild(ctx.node(), ctx.manifest())
1884 1884 self.savedirty()
1885 1885 self.ui.warn(_('qrefresh interrupted while patch was popped! '
1886 1886 '(revert --all, qpush to recover)\n'))
1887 1887 raise
1888 1888 finally:
1889 1889 wlock.release()
1890 1890 self.removeundo(repo)
1891 1891
1892 1892 def init(self, repo, create=False):
1893 1893 if not create and os.path.isdir(self.path):
1894 1894 raise error.Abort(_("patch queue directory already exists"))
1895 1895 try:
1896 1896 os.mkdir(self.path)
1897 1897 except OSError as inst:
1898 1898 if inst.errno != errno.EEXIST or not create:
1899 1899 raise
1900 1900 if create:
1901 1901 return self.qrepo(create=True)
1902 1902
1903 1903 def unapplied(self, repo, patch=None):
1904 1904 if patch and patch not in self.series:
1905 1905 raise error.Abort(_("patch %s is not in series file") % patch)
1906 1906 if not patch:
1907 1907 start = self.seriesend()
1908 1908 else:
1909 1909 start = self.series.index(patch) + 1
1910 1910 unapplied = []
1911 1911 for i in pycompat.xrange(start, len(self.series)):
1912 1912 pushable, reason = self.pushable(i)
1913 1913 if pushable:
1914 1914 unapplied.append((i, self.series[i]))
1915 1915 self.explainpushable(i)
1916 1916 return unapplied
1917 1917
1918 1918 def qseries(self, repo, missing=None, start=0, length=None, status=None,
1919 1919 summary=False):
1920 1920 def displayname(pfx, patchname, state):
1921 1921 if pfx:
1922 1922 self.ui.write(pfx)
1923 1923 if summary:
1924 1924 ph = patchheader(self.join(patchname), self.plainmode)
1925 1925 if ph.message:
1926 1926 msg = ph.message[0]
1927 1927 else:
1928 1928 msg = ''
1929 1929
1930 1930 if self.ui.formatted():
1931 1931 width = self.ui.termwidth() - len(pfx) - len(patchname) - 2
1932 1932 if width > 0:
1933 1933 msg = stringutil.ellipsis(msg, width)
1934 1934 else:
1935 1935 msg = ''
1936 1936 self.ui.write(patchname, label='qseries.' + state)
1937 1937 self.ui.write(': ')
1938 1938 self.ui.write(msg, label='qseries.message.' + state)
1939 1939 else:
1940 1940 self.ui.write(patchname, label='qseries.' + state)
1941 1941 self.ui.write('\n')
1942 1942
1943 1943 applied = set([p.name for p in self.applied])
1944 1944 if length is None:
1945 1945 length = len(self.series) - start
1946 1946 if not missing:
1947 1947 if self.ui.verbose:
1948 1948 idxwidth = len("%d" % (start + length - 1))
1949 1949 for i in pycompat.xrange(start, start + length):
1950 1950 patch = self.series[i]
1951 1951 if patch in applied:
1952 1952 char, state = 'A', 'applied'
1953 1953 elif self.pushable(i)[0]:
1954 1954 char, state = 'U', 'unapplied'
1955 1955 else:
1956 1956 char, state = 'G', 'guarded'
1957 1957 pfx = ''
1958 1958 if self.ui.verbose:
1959 1959 pfx = '%*d %s ' % (idxwidth, i, char)
1960 1960 elif status and status != char:
1961 1961 continue
1962 1962 displayname(pfx, patch, state)
1963 1963 else:
1964 1964 msng_list = []
1965 1965 for root, dirs, files in os.walk(self.path):
1966 1966 d = root[len(self.path) + 1:]
1967 1967 for f in files:
1968 1968 fl = os.path.join(d, f)
1969 1969 if (fl not in self.series and
1970 1970 fl not in (self.statuspath, self.seriespath,
1971 1971 self.guardspath)
1972 1972 and not fl.startswith('.')):
1973 1973 msng_list.append(fl)
1974 1974 for x in sorted(msng_list):
1975 1975 pfx = self.ui.verbose and ('D ') or ''
1976 1976 displayname(pfx, x, 'missing')
1977 1977
1978 1978 def issaveline(self, l):
1979 1979 if l.name == '.hg.patches.save.line':
1980 1980 return True
1981 1981
1982 1982 def qrepo(self, create=False):
1983 1983 ui = self.baseui.copy()
1984 1984 # copy back attributes set by ui.pager()
1985 1985 if self.ui.pageractive and not ui.pageractive:
1986 1986 ui.pageractive = self.ui.pageractive
1987 1987 # internal config: ui.formatted
1988 1988 ui.setconfig('ui', 'formatted',
1989 1989 self.ui.config('ui', 'formatted'), 'mqpager')
1990 1990 ui.setconfig('ui', 'interactive',
1991 1991 self.ui.config('ui', 'interactive'), 'mqpager')
1992 1992 if create or os.path.isdir(self.join(".hg")):
1993 1993 return hg.repository(ui, path=self.path, create=create)
1994 1994
1995 1995 def restore(self, repo, rev, delete=None, qupdate=None):
1996 1996 desc = repo[rev].description().strip()
1997 1997 lines = desc.splitlines()
1998 1998 i = 0
1999 1999 datastart = None
2000 2000 series = []
2001 2001 applied = []
2002 2002 qpp = None
2003 2003 for i, line in enumerate(lines):
2004 2004 if line == 'Patch Data:':
2005 2005 datastart = i + 1
2006 2006 elif line.startswith('Dirstate:'):
2007 2007 l = line.rstrip()
2008 2008 l = l[10:].split(' ')
2009 2009 qpp = [bin(x) for x in l]
2010 2010 elif datastart is not None:
2011 2011 l = line.rstrip()
2012 2012 n, name = l.split(':', 1)
2013 2013 if n:
2014 2014 applied.append(statusentry(bin(n), name))
2015 2015 else:
2016 2016 series.append(l)
2017 2017 if datastart is None:
2018 2018 self.ui.warn(_("no saved patch data found\n"))
2019 2019 return 1
2020 2020 self.ui.warn(_("restoring status: %s\n") % lines[0])
2021 2021 self.fullseries = series
2022 2022 self.applied = applied
2023 2023 self.parseseries()
2024 2024 self.seriesdirty = True
2025 2025 self.applieddirty = True
2026 2026 heads = repo.changelog.heads()
2027 2027 if delete:
2028 2028 if rev not in heads:
2029 2029 self.ui.warn(_("save entry has children, leaving it alone\n"))
2030 2030 else:
2031 2031 self.ui.warn(_("removing save entry %s\n") % short(rev))
2032 2032 pp = repo.dirstate.parents()
2033 2033 if rev in pp:
2034 2034 update = True
2035 2035 else:
2036 2036 update = False
2037 2037 strip(self.ui, repo, [rev], update=update, backup=False)
2038 2038 if qpp:
2039 2039 self.ui.warn(_("saved queue repository parents: %s %s\n") %
2040 2040 (short(qpp[0]), short(qpp[1])))
2041 2041 if qupdate:
2042 2042 self.ui.status(_("updating queue directory\n"))
2043 2043 r = self.qrepo()
2044 2044 if not r:
2045 2045 self.ui.warn(_("unable to load queue repository\n"))
2046 2046 return 1
2047 2047 hg.clean(r, qpp[0])
2048 2048
2049 2049 def save(self, repo, msg=None):
2050 2050 if not self.applied:
2051 2051 self.ui.warn(_("save: no patches applied, exiting\n"))
2052 2052 return 1
2053 2053 if self.issaveline(self.applied[-1]):
2054 2054 self.ui.warn(_("status is already saved\n"))
2055 2055 return 1
2056 2056
2057 2057 if not msg:
2058 2058 msg = _("hg patches saved state")
2059 2059 else:
2060 2060 msg = "hg patches: " + msg.rstrip('\r\n')
2061 2061 r = self.qrepo()
2062 2062 if r:
2063 2063 pp = r.dirstate.parents()
2064 2064 msg += "\nDirstate: %s %s" % (hex(pp[0]), hex(pp[1]))
2065 2065 msg += "\n\nPatch Data:\n"
2066 2066 msg += ''.join('%s\n' % x for x in self.applied)
2067 2067 msg += ''.join(':%s\n' % x for x in self.fullseries)
2068 2068 n = repo.commit(msg, force=True)
2069 2069 if not n:
2070 2070 self.ui.warn(_("repo commit failed\n"))
2071 2071 return 1
2072 2072 self.applied.append(statusentry(n, '.hg.patches.save.line'))
2073 2073 self.applieddirty = True
2074 2074 self.removeundo(repo)
2075 2075
2076 2076 def fullseriesend(self):
2077 2077 if self.applied:
2078 2078 p = self.applied[-1].name
2079 2079 end = self.findseries(p)
2080 2080 if end is None:
2081 2081 return len(self.fullseries)
2082 2082 return end + 1
2083 2083 return 0
2084 2084
2085 2085 def seriesend(self, all_patches=False):
2086 2086 """If all_patches is False, return the index of the next pushable patch
2087 2087 in the series, or the series length. If all_patches is True, return the
2088 2088 index of the first patch past the last applied one.
2089 2089 """
2090 2090 end = 0
2091 2091 def nextpatch(start):
2092 2092 if all_patches or start >= len(self.series):
2093 2093 return start
2094 2094 for i in pycompat.xrange(start, len(self.series)):
2095 2095 p, reason = self.pushable(i)
2096 2096 if p:
2097 2097 return i
2098 2098 self.explainpushable(i)
2099 2099 return len(self.series)
2100 2100 if self.applied:
2101 2101 p = self.applied[-1].name
2102 2102 try:
2103 2103 end = self.series.index(p)
2104 2104 except ValueError:
2105 2105 return 0
2106 2106 return nextpatch(end + 1)
2107 2107 return nextpatch(end)
2108 2108
2109 2109 def appliedname(self, index):
2110 2110 pname = self.applied[index].name
2111 2111 if not self.ui.verbose:
2112 2112 p = pname
2113 2113 else:
2114 2114 p = ("%d" % self.series.index(pname)) + " " + pname
2115 2115 return p
2116 2116
2117 2117 def qimport(self, repo, files, patchname=None, rev=None, existing=None,
2118 2118 force=None, git=False):
2119 2119 def checkseries(patchname):
2120 2120 if patchname in self.series:
2121 2121 raise error.Abort(_('patch %s is already in the series file')
2122 2122 % patchname)
2123 2123
2124 2124 if rev:
2125 2125 if files:
2126 2126 raise error.Abort(_('option "-r" not valid when importing '
2127 2127 'files'))
2128 2128 rev = scmutil.revrange(repo, rev)
2129 2129 rev.sort(reverse=True)
2130 2130 elif not files:
2131 2131 raise error.Abort(_('no files or revisions specified'))
2132 2132 if (len(files) > 1 or len(rev) > 1) and patchname:
2133 2133 raise error.Abort(_('option "-n" not valid when importing multiple '
2134 2134 'patches'))
2135 2135 imported = []
2136 2136 if rev:
2137 2137 # If mq patches are applied, we can only import revisions
2138 2138 # that form a linear path to qbase.
2139 2139 # Otherwise, they should form a linear path to a head.
2140 2140 heads = repo.changelog.heads(repo.changelog.node(rev.first()))
2141 2141 if len(heads) > 1:
2142 2142 raise error.Abort(_('revision %d is the root of more than one '
2143 2143 'branch') % rev.last())
2144 2144 if self.applied:
2145 2145 base = repo.changelog.node(rev.first())
2146 2146 if base in [n.node for n in self.applied]:
2147 2147 raise error.Abort(_('revision %d is already managed')
2148 2148 % rev.first())
2149 2149 if heads != [self.applied[-1].node]:
2150 2150 raise error.Abort(_('revision %d is not the parent of '
2151 2151 'the queue') % rev.first())
2152 2152 base = repo.changelog.rev(self.applied[0].node)
2153 2153 lastparent = repo.changelog.parentrevs(base)[0]
2154 2154 else:
2155 2155 if heads != [repo.changelog.node(rev.first())]:
2156 2156 raise error.Abort(_('revision %d has unmanaged children')
2157 2157 % rev.first())
2158 2158 lastparent = None
2159 2159
2160 2160 diffopts = self.diffopts({'git': git})
2161 2161 with repo.transaction('qimport') as tr:
2162 2162 for r in rev:
2163 2163 if not repo[r].mutable():
2164 2164 raise error.Abort(_('revision %d is not mutable') % r,
2165 2165 hint=_("see 'hg help phases' "
2166 2166 'for details'))
2167 2167 p1, p2 = repo.changelog.parentrevs(r)
2168 2168 n = repo.changelog.node(r)
2169 2169 if p2 != nullrev:
2170 2170 raise error.Abort(_('cannot import merge revision %d')
2171 2171 % r)
2172 2172 if lastparent and lastparent != r:
2173 2173 raise error.Abort(_('revision %d is not the parent of '
2174 2174 '%d')
2175 2175 % (r, lastparent))
2176 2176 lastparent = p1
2177 2177
2178 2178 if not patchname:
2179 2179 patchname = self.makepatchname(
2180 2180 repo[r].description().split('\n', 1)[0],
2181 2181 '%d.diff' % r)
2182 2182 checkseries(patchname)
2183 2183 self.checkpatchname(patchname, force)
2184 2184 self.fullseries.insert(0, patchname)
2185 2185
2186 2186 with self.opener(patchname, "w") as fp:
2187 2187 cmdutil.exportfile(repo, [n], fp, opts=diffopts)
2188 2188
2189 2189 se = statusentry(n, patchname)
2190 2190 self.applied.insert(0, se)
2191 2191
2192 2192 self.added.append(patchname)
2193 2193 imported.append(patchname)
2194 2194 patchname = None
2195 2195 if rev and repo.ui.configbool('mq', 'secret'):
2196 2196 # if we added anything with --rev, move the secret root
2197 2197 phases.retractboundary(repo, tr, phases.secret, [n])
2198 2198 self.parseseries()
2199 2199 self.applieddirty = True
2200 2200 self.seriesdirty = True
2201 2201
2202 2202 for i, filename in enumerate(files):
2203 2203 if existing:
2204 2204 if filename == '-':
2205 2205 raise error.Abort(_('-e is incompatible with import from -')
2206 2206 )
2207 2207 filename = normname(filename)
2208 2208 self.checkreservedname(filename)
2209 2209 if util.url(filename).islocal():
2210 2210 originpath = self.join(filename)
2211 2211 if not os.path.isfile(originpath):
2212 2212 raise error.Abort(
2213 2213 _("patch %s does not exist") % filename)
2214 2214
2215 2215 if patchname:
2216 2216 self.checkpatchname(patchname, force)
2217 2217
2218 2218 self.ui.write(_('renaming %s to %s\n')
2219 2219 % (filename, patchname))
2220 2220 util.rename(originpath, self.join(patchname))
2221 2221 else:
2222 2222 patchname = filename
2223 2223
2224 2224 else:
2225 2225 if filename == '-' and not patchname:
2226 2226 raise error.Abort(_('need --name to import a patch from -'))
2227 2227 elif not patchname:
2228 2228 patchname = normname(os.path.basename(filename.rstrip('/')))
2229 2229 self.checkpatchname(patchname, force)
2230 2230 try:
2231 2231 if filename == '-':
2232 2232 text = self.ui.fin.read()
2233 2233 else:
2234 2234 fp = hg.openpath(self.ui, filename)
2235 2235 text = fp.read()
2236 2236 fp.close()
2237 2237 except (OSError, IOError):
2238 2238 raise error.Abort(_("unable to read file %s") % filename)
2239 2239 patchf = self.opener(patchname, "w")
2240 2240 patchf.write(text)
2241 2241 patchf.close()
2242 2242 if not force:
2243 2243 checkseries(patchname)
2244 2244 if patchname not in self.series:
2245 2245 index = self.fullseriesend() + i
2246 2246 self.fullseries[index:index] = [patchname]
2247 2247 self.parseseries()
2248 2248 self.seriesdirty = True
2249 2249 self.ui.warn(_("adding %s to series file\n") % patchname)
2250 2250 self.added.append(patchname)
2251 2251 imported.append(patchname)
2252 2252 patchname = None
2253 2253
2254 2254 self.removeundo(repo)
2255 2255 return imported
2256 2256
2257 2257 def fixkeepchangesopts(ui, opts):
2258 2258 if (not ui.configbool('mq', 'keepchanges') or opts.get('force')
2259 2259 or opts.get('exact')):
2260 2260 return opts
2261 2261 opts = dict(opts)
2262 2262 opts['keep_changes'] = True
2263 2263 return opts
2264 2264
2265 2265 @command("qdelete|qremove|qrm",
2266 2266 [('k', 'keep', None, _('keep patch file')),
2267 2267 ('r', 'rev', [],
2268 2268 _('stop managing a revision (DEPRECATED)'), _('REV'))],
2269 _('hg qdelete [-k] [PATCH]...'))
2269 _('hg qdelete [-k] [PATCH]...'),
2270 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
2270 2271 def delete(ui, repo, *patches, **opts):
2271 2272 """remove patches from queue
2272 2273
2273 2274 The patches must not be applied, and at least one patch is required. Exact
2274 2275 patch identifiers must be given. With -k/--keep, the patch files are
2275 2276 preserved in the patch directory.
2276 2277
2277 2278 To stop managing a patch and move it into permanent history,
2278 2279 use the :hg:`qfinish` command."""
2279 2280 q = repo.mq
2280 2281 q.delete(repo, patches, pycompat.byteskwargs(opts))
2281 2282 q.savedirty()
2282 2283 return 0
2283 2284
2284 2285 @command("qapplied",
2285 2286 [('1', 'last', None, _('show only the preceding applied patch'))
2286 2287 ] + seriesopts,
2287 _('hg qapplied [-1] [-s] [PATCH]'))
2288 _('hg qapplied [-1] [-s] [PATCH]'),
2289 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
2288 2290 def applied(ui, repo, patch=None, **opts):
2289 2291 """print the patches already applied
2290 2292
2291 2293 Returns 0 on success."""
2292 2294
2293 2295 q = repo.mq
2294 2296 opts = pycompat.byteskwargs(opts)
2295 2297
2296 2298 if patch:
2297 2299 if patch not in q.series:
2298 2300 raise error.Abort(_("patch %s is not in series file") % patch)
2299 2301 end = q.series.index(patch) + 1
2300 2302 else:
2301 2303 end = q.seriesend(True)
2302 2304
2303 2305 if opts.get('last') and not end:
2304 2306 ui.write(_("no patches applied\n"))
2305 2307 return 1
2306 2308 elif opts.get('last') and end == 1:
2307 2309 ui.write(_("only one patch applied\n"))
2308 2310 return 1
2309 2311 elif opts.get('last'):
2310 2312 start = end - 2
2311 2313 end = 1
2312 2314 else:
2313 2315 start = 0
2314 2316
2315 2317 q.qseries(repo, length=end, start=start, status='A',
2316 2318 summary=opts.get('summary'))
2317 2319
2318 2320
2319 2321 @command("qunapplied",
2320 2322 [('1', 'first', None, _('show only the first patch'))] + seriesopts,
2321 _('hg qunapplied [-1] [-s] [PATCH]'))
2323 _('hg qunapplied [-1] [-s] [PATCH]'),
2324 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
2322 2325 def unapplied(ui, repo, patch=None, **opts):
2323 2326 """print the patches not yet applied
2324 2327
2325 2328 Returns 0 on success."""
2326 2329
2327 2330 q = repo.mq
2328 2331 opts = pycompat.byteskwargs(opts)
2329 2332 if patch:
2330 2333 if patch not in q.series:
2331 2334 raise error.Abort(_("patch %s is not in series file") % patch)
2332 2335 start = q.series.index(patch) + 1
2333 2336 else:
2334 2337 start = q.seriesend(True)
2335 2338
2336 2339 if start == len(q.series) and opts.get('first'):
2337 2340 ui.write(_("all patches applied\n"))
2338 2341 return 1
2339 2342
2340 2343 if opts.get('first'):
2341 2344 length = 1
2342 2345 else:
2343 2346 length = None
2344 2347 q.qseries(repo, start=start, length=length, status='U',
2345 2348 summary=opts.get('summary'))
2346 2349
2347 2350 @command("qimport",
2348 2351 [('e', 'existing', None, _('import file in patch directory')),
2349 2352 ('n', 'name', '',
2350 2353 _('name of patch file'), _('NAME')),
2351 2354 ('f', 'force', None, _('overwrite existing files')),
2352 2355 ('r', 'rev', [],
2353 2356 _('place existing revisions under mq control'), _('REV')),
2354 2357 ('g', 'git', None, _('use git extended diff format')),
2355 2358 ('P', 'push', None, _('qpush after importing'))],
2356 _('hg qimport [-e] [-n NAME] [-f] [-g] [-P] [-r REV]... [FILE]...'))
2359 _('hg qimport [-e] [-n NAME] [-f] [-g] [-P] [-r REV]... [FILE]...'),
2360 helpcategory=command.CATEGORY_IMPORT_EXPORT)
2357 2361 def qimport(ui, repo, *filename, **opts):
2358 2362 """import a patch or existing changeset
2359 2363
2360 2364 The patch is inserted into the series after the last applied
2361 2365 patch. If no patches have been applied, qimport prepends the patch
2362 2366 to the series.
2363 2367
2364 2368 The patch will have the same name as its source file unless you
2365 2369 give it a new one with -n/--name.
2366 2370
2367 2371 You can register an existing patch inside the patch directory with
2368 2372 the -e/--existing flag.
2369 2373
2370 2374 With -f/--force, an existing patch of the same name will be
2371 2375 overwritten.
2372 2376
2373 2377 An existing changeset may be placed under mq control with -r/--rev
2374 2378 (e.g. qimport --rev . -n patch will place the current revision
2375 2379 under mq control). With -g/--git, patches imported with --rev will
2376 2380 use the git diff format. See the diffs help topic for information
2377 2381 on why this is important for preserving rename/copy information
2378 2382 and permission changes. Use :hg:`qfinish` to remove changesets
2379 2383 from mq control.
2380 2384
2381 2385 To import a patch from standard input, pass - as the patch file.
2382 2386 When importing from standard input, a patch name must be specified
2383 2387 using the --name flag.
2384 2388
2385 2389 To import an existing patch while renaming it::
2386 2390
2387 2391 hg qimport -e existing-patch -n new-name
2388 2392
2389 2393 Returns 0 if import succeeded.
2390 2394 """
2391 2395 opts = pycompat.byteskwargs(opts)
2392 2396 with repo.lock(): # cause this may move phase
2393 2397 q = repo.mq
2394 2398 try:
2395 2399 imported = q.qimport(
2396 2400 repo, filename, patchname=opts.get('name'),
2397 2401 existing=opts.get('existing'), force=opts.get('force'),
2398 2402 rev=opts.get('rev'), git=opts.get('git'))
2399 2403 finally:
2400 2404 q.savedirty()
2401 2405
2402 2406 if imported and opts.get('push') and not opts.get('rev'):
2403 2407 return q.push(repo, imported[-1])
2404 2408 return 0
2405 2409
2406 2410 def qinit(ui, repo, create):
2407 2411 """initialize a new queue repository
2408 2412
2409 2413 This command also creates a series file for ordering patches, and
2410 2414 an mq-specific .hgignore file in the queue repository, to exclude
2411 2415 the status and guards files (these contain mostly transient state).
2412 2416
2413 2417 Returns 0 if initialization succeeded."""
2414 2418 q = repo.mq
2415 2419 r = q.init(repo, create)
2416 2420 q.savedirty()
2417 2421 if r:
2418 2422 if not os.path.exists(r.wjoin('.hgignore')):
2419 2423 fp = r.wvfs('.hgignore', 'w')
2420 2424 fp.write('^\\.hg\n')
2421 2425 fp.write('^\\.mq\n')
2422 2426 fp.write('syntax: glob\n')
2423 2427 fp.write('status\n')
2424 2428 fp.write('guards\n')
2425 2429 fp.close()
2426 2430 if not os.path.exists(r.wjoin('series')):
2427 2431 r.wvfs('series', 'w').close()
2428 2432 r[None].add(['.hgignore', 'series'])
2429 2433 commands.add(ui, r)
2430 2434 return 0
2431 2435
2432 2436 @command("^qinit",
2433 2437 [('c', 'create-repo', None, _('create queue repository'))],
2434 _('hg qinit [-c]'))
2438 _('hg qinit [-c]'),
2439 helpcategory=command.CATEGORY_REPO_CREATION)
2435 2440 def init(ui, repo, **opts):
2436 2441 """init a new queue repository (DEPRECATED)
2437 2442
2438 2443 The queue repository is unversioned by default. If
2439 2444 -c/--create-repo is specified, qinit will create a separate nested
2440 2445 repository for patches (qinit -c may also be run later to convert
2441 2446 an unversioned patch repository into a versioned one). You can use
2442 2447 qcommit to commit changes to this queue repository.
2443 2448
2444 2449 This command is deprecated. Without -c, it's implied by other relevant
2445 2450 commands. With -c, use :hg:`init --mq` instead."""
2446 2451 return qinit(ui, repo, create=opts.get(r'create_repo'))
2447 2452
2448 2453 @command("qclone",
2449 2454 [('', 'pull', None, _('use pull protocol to copy metadata')),
2450 2455 ('U', 'noupdate', None,
2451 2456 _('do not update the new working directories')),
2452 2457 ('', 'uncompressed', None,
2453 2458 _('use uncompressed transfer (fast over LAN)')),
2454 2459 ('p', 'patches', '',
2455 2460 _('location of source patch repository'), _('REPO')),
2456 2461 ] + cmdutil.remoteopts,
2457 2462 _('hg qclone [OPTION]... SOURCE [DEST]'),
2463 helpcategory=command.CATEGORY_REPO_CREATION,
2458 2464 norepo=True)
2459 2465 def clone(ui, source, dest=None, **opts):
2460 2466 '''clone main and patch repository at same time
2461 2467
2462 2468 If source is local, destination will have no patches applied. If
2463 2469 source is remote, this command can not check if patches are
2464 2470 applied in source, so cannot guarantee that patches are not
2465 2471 applied in destination. If you clone remote repository, be sure
2466 2472 before that it has no patches applied.
2467 2473
2468 2474 Source patch repository is looked for in <src>/.hg/patches by
2469 2475 default. Use -p <url> to change.
2470 2476
2471 2477 The patch directory must be a nested Mercurial repository, as
2472 2478 would be created by :hg:`init --mq`.
2473 2479
2474 2480 Return 0 on success.
2475 2481 '''
2476 2482 opts = pycompat.byteskwargs(opts)
2477 2483 def patchdir(repo):
2478 2484 """compute a patch repo url from a repo object"""
2479 2485 url = repo.url()
2480 2486 if url.endswith('/'):
2481 2487 url = url[:-1]
2482 2488 return url + '/.hg/patches'
2483 2489
2484 2490 # main repo (destination and sources)
2485 2491 if dest is None:
2486 2492 dest = hg.defaultdest(source)
2487 2493 sr = hg.peer(ui, opts, ui.expandpath(source))
2488 2494
2489 2495 # patches repo (source only)
2490 2496 if opts.get('patches'):
2491 2497 patchespath = ui.expandpath(opts.get('patches'))
2492 2498 else:
2493 2499 patchespath = patchdir(sr)
2494 2500 try:
2495 2501 hg.peer(ui, opts, patchespath)
2496 2502 except error.RepoError:
2497 2503 raise error.Abort(_('versioned patch repository not found'
2498 2504 ' (see init --mq)'))
2499 2505 qbase, destrev = None, None
2500 2506 if sr.local():
2501 2507 repo = sr.local()
2502 2508 if repo.mq.applied and repo[qbase].phase() != phases.secret:
2503 2509 qbase = repo.mq.applied[0].node
2504 2510 if not hg.islocal(dest):
2505 2511 heads = set(repo.heads())
2506 2512 destrev = list(heads.difference(repo.heads(qbase)))
2507 2513 destrev.append(repo.changelog.parents(qbase)[0])
2508 2514 elif sr.capable('lookup'):
2509 2515 try:
2510 2516 qbase = sr.lookup('qbase')
2511 2517 except error.RepoError:
2512 2518 pass
2513 2519
2514 2520 ui.note(_('cloning main repository\n'))
2515 2521 sr, dr = hg.clone(ui, opts, sr.url(), dest,
2516 2522 pull=opts.get('pull'),
2517 2523 revs=destrev,
2518 2524 update=False,
2519 2525 stream=opts.get('uncompressed'))
2520 2526
2521 2527 ui.note(_('cloning patch repository\n'))
2522 2528 hg.clone(ui, opts, opts.get('patches') or patchdir(sr), patchdir(dr),
2523 2529 pull=opts.get('pull'), update=not opts.get('noupdate'),
2524 2530 stream=opts.get('uncompressed'))
2525 2531
2526 2532 if dr.local():
2527 2533 repo = dr.local()
2528 2534 if qbase:
2529 2535 ui.note(_('stripping applied patches from destination '
2530 2536 'repository\n'))
2531 2537 strip(ui, repo, [qbase], update=False, backup=None)
2532 2538 if not opts.get('noupdate'):
2533 2539 ui.note(_('updating destination repository\n'))
2534 2540 hg.update(repo, repo.changelog.tip())
2535 2541
2536 2542 @command("qcommit|qci",
2537 2543 commands.table["^commit|ci"][1],
2538 2544 _('hg qcommit [OPTION]... [FILE]...'),
2545 helpcategory=command.CATEGORY_COMMITTING,
2539 2546 inferrepo=True)
2540 2547 def commit(ui, repo, *pats, **opts):
2541 2548 """commit changes in the queue repository (DEPRECATED)
2542 2549
2543 2550 This command is deprecated; use :hg:`commit --mq` instead."""
2544 2551 q = repo.mq
2545 2552 r = q.qrepo()
2546 2553 if not r:
2547 2554 raise error.Abort('no queue repository')
2548 2555 commands.commit(r.ui, r, *pats, **opts)
2549 2556
2550 2557 @command("qseries",
2551 2558 [('m', 'missing', None, _('print patches not in series')),
2552 2559 ] + seriesopts,
2553 _('hg qseries [-ms]'))
2560 _('hg qseries [-ms]'),
2561 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
2554 2562 def series(ui, repo, **opts):
2555 2563 """print the entire series file
2556 2564
2557 2565 Returns 0 on success."""
2558 2566 repo.mq.qseries(repo, missing=opts.get(r'missing'),
2559 2567 summary=opts.get(r'summary'))
2560 2568 return 0
2561 2569
2562 @command("qtop", seriesopts, _('hg qtop [-s]'))
2570 @command("qtop", seriesopts, _('hg qtop [-s]'),
2571 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
2563 2572 def top(ui, repo, **opts):
2564 2573 """print the name of the current patch
2565 2574
2566 2575 Returns 0 on success."""
2567 2576 q = repo.mq
2568 2577 if q.applied:
2569 2578 t = q.seriesend(True)
2570 2579 else:
2571 2580 t = 0
2572 2581
2573 2582 if t:
2574 2583 q.qseries(repo, start=t - 1, length=1, status='A',
2575 2584 summary=opts.get(r'summary'))
2576 2585 else:
2577 2586 ui.write(_("no patches applied\n"))
2578 2587 return 1
2579 2588
2580 @command("qnext", seriesopts, _('hg qnext [-s]'))
2589 @command("qnext", seriesopts, _('hg qnext [-s]'),
2590 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
2581 2591 def next(ui, repo, **opts):
2582 2592 """print the name of the next pushable patch
2583 2593
2584 2594 Returns 0 on success."""
2585 2595 q = repo.mq
2586 2596 end = q.seriesend()
2587 2597 if end == len(q.series):
2588 2598 ui.write(_("all patches applied\n"))
2589 2599 return 1
2590 2600 q.qseries(repo, start=end, length=1, summary=opts.get(r'summary'))
2591 2601
2592 @command("qprev", seriesopts, _('hg qprev [-s]'))
2602 @command("qprev", seriesopts, _('hg qprev [-s]'),
2603 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
2593 2604 def prev(ui, repo, **opts):
2594 2605 """print the name of the preceding applied patch
2595 2606
2596 2607 Returns 0 on success."""
2597 2608 q = repo.mq
2598 2609 l = len(q.applied)
2599 2610 if l == 1:
2600 2611 ui.write(_("only one patch applied\n"))
2601 2612 return 1
2602 2613 if not l:
2603 2614 ui.write(_("no patches applied\n"))
2604 2615 return 1
2605 2616 idx = q.series.index(q.applied[-2].name)
2606 2617 q.qseries(repo, start=idx, length=1, status='A',
2607 2618 summary=opts.get(r'summary'))
2608 2619
2609 2620 def setupheaderopts(ui, opts):
2610 2621 if not opts.get('user') and opts.get('currentuser'):
2611 2622 opts['user'] = ui.username()
2612 2623 if not opts.get('date') and opts.get('currentdate'):
2613 2624 opts['date'] = "%d %d" % dateutil.makedate()
2614 2625
2615 2626 @command("^qnew",
2616 2627 [('e', 'edit', None, _('invoke editor on commit messages')),
2617 2628 ('f', 'force', None, _('import uncommitted changes (DEPRECATED)')),
2618 2629 ('g', 'git', None, _('use git extended diff format')),
2619 2630 ('U', 'currentuser', None, _('add "From: <current user>" to patch')),
2620 2631 ('u', 'user', '',
2621 2632 _('add "From: <USER>" to patch'), _('USER')),
2622 2633 ('D', 'currentdate', None, _('add "Date: <current date>" to patch')),
2623 2634 ('d', 'date', '',
2624 2635 _('add "Date: <DATE>" to patch'), _('DATE'))
2625 2636 ] + cmdutil.walkopts + cmdutil.commitopts,
2626 2637 _('hg qnew [-e] [-m TEXT] [-l FILE] PATCH [FILE]...'),
2638 helpcategory=command.CATEGORY_COMMITTING,
2627 2639 inferrepo=True)
2628 2640 def new(ui, repo, patch, *args, **opts):
2629 2641 """create a new patch
2630 2642
2631 2643 qnew creates a new patch on top of the currently-applied patch (if
2632 2644 any). The patch will be initialized with any outstanding changes
2633 2645 in the working directory. You may also use -I/--include,
2634 2646 -X/--exclude, and/or a list of files after the patch name to add
2635 2647 only changes to matching files to the new patch, leaving the rest
2636 2648 as uncommitted modifications.
2637 2649
2638 2650 -u/--user and -d/--date can be used to set the (given) user and
2639 2651 date, respectively. -U/--currentuser and -D/--currentdate set user
2640 2652 to current user and date to current date.
2641 2653
2642 2654 -e/--edit, -m/--message or -l/--logfile set the patch header as
2643 2655 well as the commit message. If none is specified, the header is
2644 2656 empty and the commit message is '[mq]: PATCH'.
2645 2657
2646 2658 Use the -g/--git option to keep the patch in the git extended diff
2647 2659 format. Read the diffs help topic for more information on why this
2648 2660 is important for preserving permission changes and copy/rename
2649 2661 information.
2650 2662
2651 2663 Returns 0 on successful creation of a new patch.
2652 2664 """
2653 2665 opts = pycompat.byteskwargs(opts)
2654 2666 msg = cmdutil.logmessage(ui, opts)
2655 2667 q = repo.mq
2656 2668 opts['msg'] = msg
2657 2669 setupheaderopts(ui, opts)
2658 2670 q.new(repo, patch, *args, **pycompat.strkwargs(opts))
2659 2671 q.savedirty()
2660 2672 return 0
2661 2673
2662 2674 @command("^qrefresh",
2663 2675 [('e', 'edit', None, _('invoke editor on commit messages')),
2664 2676 ('g', 'git', None, _('use git extended diff format')),
2665 2677 ('s', 'short', None,
2666 2678 _('refresh only files already in the patch and specified files')),
2667 2679 ('U', 'currentuser', None,
2668 2680 _('add/update author field in patch with current user')),
2669 2681 ('u', 'user', '',
2670 2682 _('add/update author field in patch with given user'), _('USER')),
2671 2683 ('D', 'currentdate', None,
2672 2684 _('add/update date field in patch with current date')),
2673 2685 ('d', 'date', '',
2674 2686 _('add/update date field in patch with given date'), _('DATE'))
2675 2687 ] + cmdutil.walkopts + cmdutil.commitopts,
2676 2688 _('hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] [FILE]...'),
2689 helpcategory=command.CATEGORY_COMMITTING,
2677 2690 inferrepo=True)
2678 2691 def refresh(ui, repo, *pats, **opts):
2679 2692 """update the current patch
2680 2693
2681 2694 If any file patterns are provided, the refreshed patch will
2682 2695 contain only the modifications that match those patterns; the
2683 2696 remaining modifications will remain in the working directory.
2684 2697
2685 2698 If -s/--short is specified, files currently included in the patch
2686 2699 will be refreshed just like matched files and remain in the patch.
2687 2700
2688 2701 If -e/--edit is specified, Mercurial will start your configured editor for
2689 2702 you to enter a message. In case qrefresh fails, you will find a backup of
2690 2703 your message in ``.hg/last-message.txt``.
2691 2704
2692 2705 hg add/remove/copy/rename work as usual, though you might want to
2693 2706 use git-style patches (-g/--git or [diff] git=1) to track copies
2694 2707 and renames. See the diffs help topic for more information on the
2695 2708 git diff format.
2696 2709
2697 2710 Returns 0 on success.
2698 2711 """
2699 2712 opts = pycompat.byteskwargs(opts)
2700 2713 q = repo.mq
2701 2714 message = cmdutil.logmessage(ui, opts)
2702 2715 setupheaderopts(ui, opts)
2703 2716 with repo.wlock():
2704 2717 ret = q.refresh(repo, pats, msg=message, **pycompat.strkwargs(opts))
2705 2718 q.savedirty()
2706 2719 return ret
2707 2720
2708 2721 @command("^qdiff",
2709 2722 cmdutil.diffopts + cmdutil.diffopts2 + cmdutil.walkopts,
2710 2723 _('hg qdiff [OPTION]... [FILE]...'),
2724 helpcategory=command.CATEGORY_FILE_CONTENTS,
2711 2725 inferrepo=True)
2712 2726 def diff(ui, repo, *pats, **opts):
2713 2727 """diff of the current patch and subsequent modifications
2714 2728
2715 2729 Shows a diff which includes the current patch as well as any
2716 2730 changes which have been made in the working directory since the
2717 2731 last refresh (thus showing what the current patch would become
2718 2732 after a qrefresh).
2719 2733
2720 2734 Use :hg:`diff` if you only want to see the changes made since the
2721 2735 last qrefresh, or :hg:`export qtip` if you want to see changes
2722 2736 made by the current patch without including changes made since the
2723 2737 qrefresh.
2724 2738
2725 2739 Returns 0 on success.
2726 2740 """
2727 2741 ui.pager('qdiff')
2728 2742 repo.mq.diff(repo, pats, pycompat.byteskwargs(opts))
2729 2743 return 0
2730 2744
2731 2745 @command('qfold',
2732 2746 [('e', 'edit', None, _('invoke editor on commit messages')),
2733 2747 ('k', 'keep', None, _('keep folded patch files')),
2734 2748 ] + cmdutil.commitopts,
2735 _('hg qfold [-e] [-k] [-m TEXT] [-l FILE] PATCH...'))
2749 _('hg qfold [-e] [-k] [-m TEXT] [-l FILE] PATCH...'),
2750 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
2736 2751 def fold(ui, repo, *files, **opts):
2737 2752 """fold the named patches into the current patch
2738 2753
2739 2754 Patches must not yet be applied. Each patch will be successively
2740 2755 applied to the current patch in the order given. If all the
2741 2756 patches apply successfully, the current patch will be refreshed
2742 2757 with the new cumulative patch, and the folded patches will be
2743 2758 deleted. With -k/--keep, the folded patch files will not be
2744 2759 removed afterwards.
2745 2760
2746 2761 The header for each folded patch will be concatenated with the
2747 2762 current patch header, separated by a line of ``* * *``.
2748 2763
2749 2764 Returns 0 on success."""
2750 2765 opts = pycompat.byteskwargs(opts)
2751 2766 q = repo.mq
2752 2767 if not files:
2753 2768 raise error.Abort(_('qfold requires at least one patch name'))
2754 2769 if not q.checktoppatch(repo)[0]:
2755 2770 raise error.Abort(_('no patches applied'))
2756 2771 q.checklocalchanges(repo)
2757 2772
2758 2773 message = cmdutil.logmessage(ui, opts)
2759 2774
2760 2775 parent = q.lookup('qtip')
2761 2776 patches = []
2762 2777 messages = []
2763 2778 for f in files:
2764 2779 p = q.lookup(f)
2765 2780 if p in patches or p == parent:
2766 2781 ui.warn(_('skipping already folded patch %s\n') % p)
2767 2782 if q.isapplied(p):
2768 2783 raise error.Abort(_('qfold cannot fold already applied patch %s')
2769 2784 % p)
2770 2785 patches.append(p)
2771 2786
2772 2787 for p in patches:
2773 2788 if not message:
2774 2789 ph = patchheader(q.join(p), q.plainmode)
2775 2790 if ph.message:
2776 2791 messages.append(ph.message)
2777 2792 pf = q.join(p)
2778 2793 (patchsuccess, files, fuzz) = q.patch(repo, pf)
2779 2794 if not patchsuccess:
2780 2795 raise error.Abort(_('error folding patch %s') % p)
2781 2796
2782 2797 if not message:
2783 2798 ph = patchheader(q.join(parent), q.plainmode)
2784 2799 message = ph.message
2785 2800 for msg in messages:
2786 2801 if msg:
2787 2802 if message:
2788 2803 message.append('* * *')
2789 2804 message.extend(msg)
2790 2805 message = '\n'.join(message)
2791 2806
2792 2807 diffopts = q.patchopts(q.diffopts(), *patches)
2793 2808 with repo.wlock():
2794 2809 q.refresh(repo, msg=message, git=diffopts.git, edit=opts.get('edit'),
2795 2810 editform='mq.qfold')
2796 2811 q.delete(repo, patches, opts)
2797 2812 q.savedirty()
2798 2813
2799 2814 @command("qgoto",
2800 2815 [('', 'keep-changes', None,
2801 2816 _('tolerate non-conflicting local changes')),
2802 2817 ('f', 'force', None, _('overwrite any local changes')),
2803 2818 ('', 'no-backup', None, _('do not save backup copies of files'))],
2804 _('hg qgoto [OPTION]... PATCH'))
2819 _('hg qgoto [OPTION]... PATCH'),
2820 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
2805 2821 def goto(ui, repo, patch, **opts):
2806 2822 '''push or pop patches until named patch is at top of stack
2807 2823
2808 2824 Returns 0 on success.'''
2809 2825 opts = pycompat.byteskwargs(opts)
2810 2826 opts = fixkeepchangesopts(ui, opts)
2811 2827 q = repo.mq
2812 2828 patch = q.lookup(patch)
2813 2829 nobackup = opts.get('no_backup')
2814 2830 keepchanges = opts.get('keep_changes')
2815 2831 if q.isapplied(patch):
2816 2832 ret = q.pop(repo, patch, force=opts.get('force'), nobackup=nobackup,
2817 2833 keepchanges=keepchanges)
2818 2834 else:
2819 2835 ret = q.push(repo, patch, force=opts.get('force'), nobackup=nobackup,
2820 2836 keepchanges=keepchanges)
2821 2837 q.savedirty()
2822 2838 return ret
2823 2839
2824 2840 @command("qguard",
2825 2841 [('l', 'list', None, _('list all patches and guards')),
2826 2842 ('n', 'none', None, _('drop all guards'))],
2827 _('hg qguard [-l] [-n] [PATCH] [-- [+GUARD]... [-GUARD]...]'))
2843 _('hg qguard [-l] [-n] [PATCH] [-- [+GUARD]... [-GUARD]...]'),
2844 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
2828 2845 def guard(ui, repo, *args, **opts):
2829 2846 '''set or print guards for a patch
2830 2847
2831 2848 Guards control whether a patch can be pushed. A patch with no
2832 2849 guards is always pushed. A patch with a positive guard ("+foo") is
2833 2850 pushed only if the :hg:`qselect` command has activated it. A patch with
2834 2851 a negative guard ("-foo") is never pushed if the :hg:`qselect` command
2835 2852 has activated it.
2836 2853
2837 2854 With no arguments, print the currently active guards.
2838 2855 With arguments, set guards for the named patch.
2839 2856
2840 2857 .. note::
2841 2858
2842 2859 Specifying negative guards now requires '--'.
2843 2860
2844 2861 To set guards on another patch::
2845 2862
2846 2863 hg qguard other.patch -- +2.6.17 -stable
2847 2864
2848 2865 Returns 0 on success.
2849 2866 '''
2850 2867 def status(idx):
2851 2868 guards = q.seriesguards[idx] or ['unguarded']
2852 2869 if q.series[idx] in applied:
2853 2870 state = 'applied'
2854 2871 elif q.pushable(idx)[0]:
2855 2872 state = 'unapplied'
2856 2873 else:
2857 2874 state = 'guarded'
2858 2875 label = 'qguard.patch qguard.%s qseries.%s' % (state, state)
2859 2876 ui.write('%s: ' % ui.label(q.series[idx], label))
2860 2877
2861 2878 for i, guard in enumerate(guards):
2862 2879 if guard.startswith('+'):
2863 2880 ui.write(guard, label='qguard.positive')
2864 2881 elif guard.startswith('-'):
2865 2882 ui.write(guard, label='qguard.negative')
2866 2883 else:
2867 2884 ui.write(guard, label='qguard.unguarded')
2868 2885 if i != len(guards) - 1:
2869 2886 ui.write(' ')
2870 2887 ui.write('\n')
2871 2888 q = repo.mq
2872 2889 applied = set(p.name for p in q.applied)
2873 2890 patch = None
2874 2891 args = list(args)
2875 2892 if opts.get(r'list'):
2876 2893 if args or opts.get(r'none'):
2877 2894 raise error.Abort(_('cannot mix -l/--list with options or '
2878 2895 'arguments'))
2879 2896 for i in pycompat.xrange(len(q.series)):
2880 2897 status(i)
2881 2898 return
2882 2899 if not args or args[0][0:1] in '-+':
2883 2900 if not q.applied:
2884 2901 raise error.Abort(_('no patches applied'))
2885 2902 patch = q.applied[-1].name
2886 2903 if patch is None and args[0][0:1] not in '-+':
2887 2904 patch = args.pop(0)
2888 2905 if patch is None:
2889 2906 raise error.Abort(_('no patch to work with'))
2890 2907 if args or opts.get(r'none'):
2891 2908 idx = q.findseries(patch)
2892 2909 if idx is None:
2893 2910 raise error.Abort(_('no patch named %s') % patch)
2894 2911 q.setguards(idx, args)
2895 2912 q.savedirty()
2896 2913 else:
2897 2914 status(q.series.index(q.lookup(patch)))
2898 2915
2899 @command("qheader", [], _('hg qheader [PATCH]'))
2916 @command("qheader", [], _('hg qheader [PATCH]'),
2917 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
2900 2918 def header(ui, repo, patch=None):
2901 2919 """print the header of the topmost or specified patch
2902 2920
2903 2921 Returns 0 on success."""
2904 2922 q = repo.mq
2905 2923
2906 2924 if patch:
2907 2925 patch = q.lookup(patch)
2908 2926 else:
2909 2927 if not q.applied:
2910 2928 ui.write(_('no patches applied\n'))
2911 2929 return 1
2912 2930 patch = q.lookup('qtip')
2913 2931 ph = patchheader(q.join(patch), q.plainmode)
2914 2932
2915 2933 ui.write('\n'.join(ph.message) + '\n')
2916 2934
2917 2935 def lastsavename(path):
2918 2936 (directory, base) = os.path.split(path)
2919 2937 names = os.listdir(directory)
2920 2938 namere = re.compile("%s.([0-9]+)" % base)
2921 2939 maxindex = None
2922 2940 maxname = None
2923 2941 for f in names:
2924 2942 m = namere.match(f)
2925 2943 if m:
2926 2944 index = int(m.group(1))
2927 2945 if maxindex is None or index > maxindex:
2928 2946 maxindex = index
2929 2947 maxname = f
2930 2948 if maxname:
2931 2949 return (os.path.join(directory, maxname), maxindex)
2932 2950 return (None, None)
2933 2951
2934 2952 def savename(path):
2935 2953 (last, index) = lastsavename(path)
2936 2954 if last is None:
2937 2955 index = 0
2938 2956 newpath = path + ".%d" % (index + 1)
2939 2957 return newpath
2940 2958
2941 2959 @command("^qpush",
2942 2960 [('', 'keep-changes', None,
2943 2961 _('tolerate non-conflicting local changes')),
2944 2962 ('f', 'force', None, _('apply on top of local changes')),
2945 2963 ('e', 'exact', None,
2946 2964 _('apply the target patch to its recorded parent')),
2947 2965 ('l', 'list', None, _('list patch name in commit text')),
2948 2966 ('a', 'all', None, _('apply all patches')),
2949 2967 ('m', 'merge', None, _('merge from another queue (DEPRECATED)')),
2950 2968 ('n', 'name', '',
2951 2969 _('merge queue name (DEPRECATED)'), _('NAME')),
2952 2970 ('', 'move', None,
2953 2971 _('reorder patch series and apply only the patch')),
2954 2972 ('', 'no-backup', None, _('do not save backup copies of files'))],
2955 _('hg qpush [-f] [-l] [-a] [--move] [PATCH | INDEX]'))
2973 _('hg qpush [-f] [-l] [-a] [--move] [PATCH | INDEX]'),
2974 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
2956 2975 def push(ui, repo, patch=None, **opts):
2957 2976 """push the next patch onto the stack
2958 2977
2959 2978 By default, abort if the working directory contains uncommitted
2960 2979 changes. With --keep-changes, abort only if the uncommitted files
2961 2980 overlap with patched files. With -f/--force, backup and patch over
2962 2981 uncommitted changes.
2963 2982
2964 2983 Return 0 on success.
2965 2984 """
2966 2985 q = repo.mq
2967 2986 mergeq = None
2968 2987
2969 2988 opts = pycompat.byteskwargs(opts)
2970 2989 opts = fixkeepchangesopts(ui, opts)
2971 2990 if opts.get('merge'):
2972 2991 if opts.get('name'):
2973 2992 newpath = repo.vfs.join(opts.get('name'))
2974 2993 else:
2975 2994 newpath, i = lastsavename(q.path)
2976 2995 if not newpath:
2977 2996 ui.warn(_("no saved queues found, please use -n\n"))
2978 2997 return 1
2979 2998 mergeq = queue(ui, repo.baseui, repo.path, newpath)
2980 2999 ui.warn(_("merging with queue at: %s\n") % mergeq.path)
2981 3000 ret = q.push(repo, patch, force=opts.get('force'), list=opts.get('list'),
2982 3001 mergeq=mergeq, all=opts.get('all'), move=opts.get('move'),
2983 3002 exact=opts.get('exact'), nobackup=opts.get('no_backup'),
2984 3003 keepchanges=opts.get('keep_changes'))
2985 3004 return ret
2986 3005
2987 3006 @command("^qpop",
2988 3007 [('a', 'all', None, _('pop all patches')),
2989 3008 ('n', 'name', '',
2990 3009 _('queue name to pop (DEPRECATED)'), _('NAME')),
2991 3010 ('', 'keep-changes', None,
2992 3011 _('tolerate non-conflicting local changes')),
2993 3012 ('f', 'force', None, _('forget any local changes to patched files')),
2994 3013 ('', 'no-backup', None, _('do not save backup copies of files'))],
2995 _('hg qpop [-a] [-f] [PATCH | INDEX]'))
3014 _('hg qpop [-a] [-f] [PATCH | INDEX]'),
3015 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
2996 3016 def pop(ui, repo, patch=None, **opts):
2997 3017 """pop the current patch off the stack
2998 3018
2999 3019 Without argument, pops off the top of the patch stack. If given a
3000 3020 patch name, keeps popping off patches until the named patch is at
3001 3021 the top of the stack.
3002 3022
3003 3023 By default, abort if the working directory contains uncommitted
3004 3024 changes. With --keep-changes, abort only if the uncommitted files
3005 3025 overlap with patched files. With -f/--force, backup and discard
3006 3026 changes made to such files.
3007 3027
3008 3028 Return 0 on success.
3009 3029 """
3010 3030 opts = pycompat.byteskwargs(opts)
3011 3031 opts = fixkeepchangesopts(ui, opts)
3012 3032 localupdate = True
3013 3033 if opts.get('name'):
3014 3034 q = queue(ui, repo.baseui, repo.path, repo.vfs.join(opts.get('name')))
3015 3035 ui.warn(_('using patch queue: %s\n') % q.path)
3016 3036 localupdate = False
3017 3037 else:
3018 3038 q = repo.mq
3019 3039 ret = q.pop(repo, patch, force=opts.get('force'), update=localupdate,
3020 3040 all=opts.get('all'), nobackup=opts.get('no_backup'),
3021 3041 keepchanges=opts.get('keep_changes'))
3022 3042 q.savedirty()
3023 3043 return ret
3024 3044
3025 @command("qrename|qmv", [], _('hg qrename PATCH1 [PATCH2]'))
3045 @command("qrename|qmv", [], _('hg qrename PATCH1 [PATCH2]'),
3046 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
3026 3047 def rename(ui, repo, patch, name=None, **opts):
3027 3048 """rename a patch
3028 3049
3029 3050 With one argument, renames the current patch to PATCH1.
3030 3051 With two arguments, renames PATCH1 to PATCH2.
3031 3052
3032 3053 Returns 0 on success."""
3033 3054 q = repo.mq
3034 3055 if not name:
3035 3056 name = patch
3036 3057 patch = None
3037 3058
3038 3059 if patch:
3039 3060 patch = q.lookup(patch)
3040 3061 else:
3041 3062 if not q.applied:
3042 3063 ui.write(_('no patches applied\n'))
3043 3064 return
3044 3065 patch = q.lookup('qtip')
3045 3066 absdest = q.join(name)
3046 3067 if os.path.isdir(absdest):
3047 3068 name = normname(os.path.join(name, os.path.basename(patch)))
3048 3069 absdest = q.join(name)
3049 3070 q.checkpatchname(name)
3050 3071
3051 3072 ui.note(_('renaming %s to %s\n') % (patch, name))
3052 3073 i = q.findseries(patch)
3053 3074 guards = q.guard_re.findall(q.fullseries[i])
3054 3075 q.fullseries[i] = name + ''.join([' #' + g for g in guards])
3055 3076 q.parseseries()
3056 3077 q.seriesdirty = True
3057 3078
3058 3079 info = q.isapplied(patch)
3059 3080 if info:
3060 3081 q.applied[info[0]] = statusentry(info[1], name)
3061 3082 q.applieddirty = True
3062 3083
3063 3084 destdir = os.path.dirname(absdest)
3064 3085 if not os.path.isdir(destdir):
3065 3086 os.makedirs(destdir)
3066 3087 util.rename(q.join(patch), absdest)
3067 3088 r = q.qrepo()
3068 3089 if r and patch in r.dirstate:
3069 3090 wctx = r[None]
3070 3091 with r.wlock():
3071 3092 if r.dirstate[patch] == 'a':
3072 3093 r.dirstate.drop(patch)
3073 3094 r.dirstate.add(name)
3074 3095 else:
3075 3096 wctx.copy(patch, name)
3076 3097 wctx.forget([patch])
3077 3098
3078 3099 q.savedirty()
3079 3100
3080 3101 @command("qrestore",
3081 3102 [('d', 'delete', None, _('delete save entry')),
3082 3103 ('u', 'update', None, _('update queue working directory'))],
3083 _('hg qrestore [-d] [-u] REV'))
3104 _('hg qrestore [-d] [-u] REV'),
3105 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
3084 3106 def restore(ui, repo, rev, **opts):
3085 3107 """restore the queue state saved by a revision (DEPRECATED)
3086 3108
3087 3109 This command is deprecated, use :hg:`rebase` instead."""
3088 3110 rev = repo.lookup(rev)
3089 3111 q = repo.mq
3090 3112 q.restore(repo, rev, delete=opts.get(r'delete'),
3091 3113 qupdate=opts.get(r'update'))
3092 3114 q.savedirty()
3093 3115 return 0
3094 3116
3095 3117 @command("qsave",
3096 3118 [('c', 'copy', None, _('copy patch directory')),
3097 3119 ('n', 'name', '',
3098 3120 _('copy directory name'), _('NAME')),
3099 3121 ('e', 'empty', None, _('clear queue status file')),
3100 3122 ('f', 'force', None, _('force copy'))] + cmdutil.commitopts,
3101 _('hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'))
3123 _('hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'),
3124 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
3102 3125 def save(ui, repo, **opts):
3103 3126 """save current queue state (DEPRECATED)
3104 3127
3105 3128 This command is deprecated, use :hg:`rebase` instead."""
3106 3129 q = repo.mq
3107 3130 opts = pycompat.byteskwargs(opts)
3108 3131 message = cmdutil.logmessage(ui, opts)
3109 3132 ret = q.save(repo, msg=message)
3110 3133 if ret:
3111 3134 return ret
3112 3135 q.savedirty() # save to .hg/patches before copying
3113 3136 if opts.get('copy'):
3114 3137 path = q.path
3115 3138 if opts.get('name'):
3116 3139 newpath = os.path.join(q.basepath, opts.get('name'))
3117 3140 if os.path.exists(newpath):
3118 3141 if not os.path.isdir(newpath):
3119 3142 raise error.Abort(_('destination %s exists and is not '
3120 3143 'a directory') % newpath)
3121 3144 if not opts.get('force'):
3122 3145 raise error.Abort(_('destination %s exists, '
3123 3146 'use -f to force') % newpath)
3124 3147 else:
3125 3148 newpath = savename(path)
3126 3149 ui.warn(_("copy %s to %s\n") % (path, newpath))
3127 3150 util.copyfiles(path, newpath)
3128 3151 if opts.get('empty'):
3129 3152 del q.applied[:]
3130 3153 q.applieddirty = True
3131 3154 q.savedirty()
3132 3155 return 0
3133 3156
3134 3157
3135 3158 @command("qselect",
3136 3159 [('n', 'none', None, _('disable all guards')),
3137 3160 ('s', 'series', None, _('list all guards in series file')),
3138 3161 ('', 'pop', None, _('pop to before first guarded applied patch')),
3139 3162 ('', 'reapply', None, _('pop, then reapply patches'))],
3140 _('hg qselect [OPTION]... [GUARD]...'))
3163 _('hg qselect [OPTION]... [GUARD]...'),
3164 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
3141 3165 def select(ui, repo, *args, **opts):
3142 3166 '''set or print guarded patches to push
3143 3167
3144 3168 Use the :hg:`qguard` command to set or print guards on patch, then use
3145 3169 qselect to tell mq which guards to use. A patch will be pushed if
3146 3170 it has no guards or any positive guards match the currently
3147 3171 selected guard, but will not be pushed if any negative guards
3148 3172 match the current guard. For example::
3149 3173
3150 3174 qguard foo.patch -- -stable (negative guard)
3151 3175 qguard bar.patch +stable (positive guard)
3152 3176 qselect stable
3153 3177
3154 3178 This activates the "stable" guard. mq will skip foo.patch (because
3155 3179 it has a negative match) but push bar.patch (because it has a
3156 3180 positive match).
3157 3181
3158 3182 With no arguments, prints the currently active guards.
3159 3183 With one argument, sets the active guard.
3160 3184
3161 3185 Use -n/--none to deactivate guards (no other arguments needed).
3162 3186 When no guards are active, patches with positive guards are
3163 3187 skipped and patches with negative guards are pushed.
3164 3188
3165 3189 qselect can change the guards on applied patches. It does not pop
3166 3190 guarded patches by default. Use --pop to pop back to the last
3167 3191 applied patch that is not guarded. Use --reapply (which implies
3168 3192 --pop) to push back to the current patch afterwards, but skip
3169 3193 guarded patches.
3170 3194
3171 3195 Use -s/--series to print a list of all guards in the series file
3172 3196 (no other arguments needed). Use -v for more information.
3173 3197
3174 3198 Returns 0 on success.'''
3175 3199
3176 3200 q = repo.mq
3177 3201 opts = pycompat.byteskwargs(opts)
3178 3202 guards = q.active()
3179 3203 pushable = lambda i: q.pushable(q.applied[i].name)[0]
3180 3204 if args or opts.get('none'):
3181 3205 old_unapplied = q.unapplied(repo)
3182 3206 old_guarded = [i for i in pycompat.xrange(len(q.applied))
3183 3207 if not pushable(i)]
3184 3208 q.setactive(args)
3185 3209 q.savedirty()
3186 3210 if not args:
3187 3211 ui.status(_('guards deactivated\n'))
3188 3212 if not opts.get('pop') and not opts.get('reapply'):
3189 3213 unapplied = q.unapplied(repo)
3190 3214 guarded = [i for i in pycompat.xrange(len(q.applied))
3191 3215 if not pushable(i)]
3192 3216 if len(unapplied) != len(old_unapplied):
3193 3217 ui.status(_('number of unguarded, unapplied patches has '
3194 3218 'changed from %d to %d\n') %
3195 3219 (len(old_unapplied), len(unapplied)))
3196 3220 if len(guarded) != len(old_guarded):
3197 3221 ui.status(_('number of guarded, applied patches has changed '
3198 3222 'from %d to %d\n') %
3199 3223 (len(old_guarded), len(guarded)))
3200 3224 elif opts.get('series'):
3201 3225 guards = {}
3202 3226 noguards = 0
3203 3227 for gs in q.seriesguards:
3204 3228 if not gs:
3205 3229 noguards += 1
3206 3230 for g in gs:
3207 3231 guards.setdefault(g, 0)
3208 3232 guards[g] += 1
3209 3233 if ui.verbose:
3210 3234 guards['NONE'] = noguards
3211 3235 guards = list(guards.items())
3212 3236 guards.sort(key=lambda x: x[0][1:])
3213 3237 if guards:
3214 3238 ui.note(_('guards in series file:\n'))
3215 3239 for guard, count in guards:
3216 3240 ui.note('%2d ' % count)
3217 3241 ui.write(guard, '\n')
3218 3242 else:
3219 3243 ui.note(_('no guards in series file\n'))
3220 3244 else:
3221 3245 if guards:
3222 3246 ui.note(_('active guards:\n'))
3223 3247 for g in guards:
3224 3248 ui.write(g, '\n')
3225 3249 else:
3226 3250 ui.write(_('no active guards\n'))
3227 3251 reapply = opts.get('reapply') and q.applied and q.applied[-1].name
3228 3252 popped = False
3229 3253 if opts.get('pop') or opts.get('reapply'):
3230 3254 for i in pycompat.xrange(len(q.applied)):
3231 3255 if not pushable(i):
3232 3256 ui.status(_('popping guarded patches\n'))
3233 3257 popped = True
3234 3258 if i == 0:
3235 3259 q.pop(repo, all=True)
3236 3260 else:
3237 3261 q.pop(repo, q.applied[i - 1].name)
3238 3262 break
3239 3263 if popped:
3240 3264 try:
3241 3265 if reapply:
3242 3266 ui.status(_('reapplying unguarded patches\n'))
3243 3267 q.push(repo, reapply)
3244 3268 finally:
3245 3269 q.savedirty()
3246 3270
3247 3271 @command("qfinish",
3248 3272 [('a', 'applied', None, _('finish all applied changesets'))],
3249 _('hg qfinish [-a] [REV]...'))
3273 _('hg qfinish [-a] [REV]...'),
3274 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
3250 3275 def finish(ui, repo, *revrange, **opts):
3251 3276 """move applied patches into repository history
3252 3277
3253 3278 Finishes the specified revisions (corresponding to applied
3254 3279 patches) by moving them out of mq control into regular repository
3255 3280 history.
3256 3281
3257 3282 Accepts a revision range or the -a/--applied option. If --applied
3258 3283 is specified, all applied mq revisions are removed from mq
3259 3284 control. Otherwise, the given revisions must be at the base of the
3260 3285 stack of applied patches.
3261 3286
3262 3287 This can be especially useful if your changes have been applied to
3263 3288 an upstream repository, or if you are about to push your changes
3264 3289 to upstream.
3265 3290
3266 3291 Returns 0 on success.
3267 3292 """
3268 3293 if not opts.get(r'applied') and not revrange:
3269 3294 raise error.Abort(_('no revisions specified'))
3270 3295 elif opts.get(r'applied'):
3271 3296 revrange = ('qbase::qtip',) + revrange
3272 3297
3273 3298 q = repo.mq
3274 3299 if not q.applied:
3275 3300 ui.status(_('no patches applied\n'))
3276 3301 return 0
3277 3302
3278 3303 revs = scmutil.revrange(repo, revrange)
3279 3304 if repo['.'].rev() in revs and repo[None].files():
3280 3305 ui.warn(_('warning: uncommitted changes in the working directory\n'))
3281 3306 # queue.finish may changes phases but leave the responsibility to lock the
3282 3307 # repo to the caller to avoid deadlock with wlock. This command code is
3283 3308 # responsibility for this locking.
3284 3309 with repo.lock():
3285 3310 q.finish(repo, revs)
3286 3311 q.savedirty()
3287 3312 return 0
3288 3313
3289 3314 @command("qqueue",
3290 3315 [('l', 'list', False, _('list all available queues')),
3291 3316 ('', 'active', False, _('print name of active queue')),
3292 3317 ('c', 'create', False, _('create new queue')),
3293 3318 ('', 'rename', False, _('rename active queue')),
3294 3319 ('', 'delete', False, _('delete reference to queue')),
3295 3320 ('', 'purge', False, _('delete queue, and remove patch dir')),
3296 3321 ],
3297 _('[OPTION] [QUEUE]'))
3322 _('[OPTION] [QUEUE]'),
3323 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION)
3298 3324 def qqueue(ui, repo, name=None, **opts):
3299 3325 '''manage multiple patch queues
3300 3326
3301 3327 Supports switching between different patch queues, as well as creating
3302 3328 new patch queues and deleting existing ones.
3303 3329
3304 3330 Omitting a queue name or specifying -l/--list will show you the registered
3305 3331 queues - by default the "normal" patches queue is registered. The currently
3306 3332 active queue will be marked with "(active)". Specifying --active will print
3307 3333 only the name of the active queue.
3308 3334
3309 3335 To create a new queue, use -c/--create. The queue is automatically made
3310 3336 active, except in the case where there are applied patches from the
3311 3337 currently active queue in the repository. Then the queue will only be
3312 3338 created and switching will fail.
3313 3339
3314 3340 To delete an existing queue, use --delete. You cannot delete the currently
3315 3341 active queue.
3316 3342
3317 3343 Returns 0 on success.
3318 3344 '''
3319 3345 q = repo.mq
3320 3346 _defaultqueue = 'patches'
3321 3347 _allqueues = 'patches.queues'
3322 3348 _activequeue = 'patches.queue'
3323 3349
3324 3350 def _getcurrent():
3325 3351 cur = os.path.basename(q.path)
3326 3352 if cur.startswith('patches-'):
3327 3353 cur = cur[8:]
3328 3354 return cur
3329 3355
3330 3356 def _noqueues():
3331 3357 try:
3332 3358 fh = repo.vfs(_allqueues, 'r')
3333 3359 fh.close()
3334 3360 except IOError:
3335 3361 return True
3336 3362
3337 3363 return False
3338 3364
3339 3365 def _getqueues():
3340 3366 current = _getcurrent()
3341 3367
3342 3368 try:
3343 3369 fh = repo.vfs(_allqueues, 'r')
3344 3370 queues = [queue.strip() for queue in fh if queue.strip()]
3345 3371 fh.close()
3346 3372 if current not in queues:
3347 3373 queues.append(current)
3348 3374 except IOError:
3349 3375 queues = [_defaultqueue]
3350 3376
3351 3377 return sorted(queues)
3352 3378
3353 3379 def _setactive(name):
3354 3380 if q.applied:
3355 3381 raise error.Abort(_('new queue created, but cannot make active '
3356 3382 'as patches are applied'))
3357 3383 _setactivenocheck(name)
3358 3384
3359 3385 def _setactivenocheck(name):
3360 3386 fh = repo.vfs(_activequeue, 'w')
3361 3387 if name != 'patches':
3362 3388 fh.write(name)
3363 3389 fh.close()
3364 3390
3365 3391 def _addqueue(name):
3366 3392 fh = repo.vfs(_allqueues, 'a')
3367 3393 fh.write('%s\n' % (name,))
3368 3394 fh.close()
3369 3395
3370 3396 def _queuedir(name):
3371 3397 if name == 'patches':
3372 3398 return repo.vfs.join('patches')
3373 3399 else:
3374 3400 return repo.vfs.join('patches-' + name)
3375 3401
3376 3402 def _validname(name):
3377 3403 for n in name:
3378 3404 if n in ':\\/.':
3379 3405 return False
3380 3406 return True
3381 3407
3382 3408 def _delete(name):
3383 3409 if name not in existing:
3384 3410 raise error.Abort(_('cannot delete queue that does not exist'))
3385 3411
3386 3412 current = _getcurrent()
3387 3413
3388 3414 if name == current:
3389 3415 raise error.Abort(_('cannot delete currently active queue'))
3390 3416
3391 3417 fh = repo.vfs('patches.queues.new', 'w')
3392 3418 for queue in existing:
3393 3419 if queue == name:
3394 3420 continue
3395 3421 fh.write('%s\n' % (queue,))
3396 3422 fh.close()
3397 3423 repo.vfs.rename('patches.queues.new', _allqueues)
3398 3424
3399 3425 opts = pycompat.byteskwargs(opts)
3400 3426 if not name or opts.get('list') or opts.get('active'):
3401 3427 current = _getcurrent()
3402 3428 if opts.get('active'):
3403 3429 ui.write('%s\n' % (current,))
3404 3430 return
3405 3431 for queue in _getqueues():
3406 3432 ui.write('%s' % (queue,))
3407 3433 if queue == current and not ui.quiet:
3408 3434 ui.write(_(' (active)\n'))
3409 3435 else:
3410 3436 ui.write('\n')
3411 3437 return
3412 3438
3413 3439 if not _validname(name):
3414 3440 raise error.Abort(
3415 3441 _('invalid queue name, may not contain the characters ":\\/."'))
3416 3442
3417 3443 with repo.wlock():
3418 3444 existing = _getqueues()
3419 3445
3420 3446 if opts.get('create'):
3421 3447 if name in existing:
3422 3448 raise error.Abort(_('queue "%s" already exists') % name)
3423 3449 if _noqueues():
3424 3450 _addqueue(_defaultqueue)
3425 3451 _addqueue(name)
3426 3452 _setactive(name)
3427 3453 elif opts.get('rename'):
3428 3454 current = _getcurrent()
3429 3455 if name == current:
3430 3456 raise error.Abort(_('can\'t rename "%s" to its current name')
3431 3457 % name)
3432 3458 if name in existing:
3433 3459 raise error.Abort(_('queue "%s" already exists') % name)
3434 3460
3435 3461 olddir = _queuedir(current)
3436 3462 newdir = _queuedir(name)
3437 3463
3438 3464 if os.path.exists(newdir):
3439 3465 raise error.Abort(_('non-queue directory "%s" already exists') %
3440 3466 newdir)
3441 3467
3442 3468 fh = repo.vfs('patches.queues.new', 'w')
3443 3469 for queue in existing:
3444 3470 if queue == current:
3445 3471 fh.write('%s\n' % (name,))
3446 3472 if os.path.exists(olddir):
3447 3473 util.rename(olddir, newdir)
3448 3474 else:
3449 3475 fh.write('%s\n' % (queue,))
3450 3476 fh.close()
3451 3477 repo.vfs.rename('patches.queues.new', _allqueues)
3452 3478 _setactivenocheck(name)
3453 3479 elif opts.get('delete'):
3454 3480 _delete(name)
3455 3481 elif opts.get('purge'):
3456 3482 if name in existing:
3457 3483 _delete(name)
3458 3484 qdir = _queuedir(name)
3459 3485 if os.path.exists(qdir):
3460 3486 shutil.rmtree(qdir)
3461 3487 else:
3462 3488 if name not in existing:
3463 3489 raise error.Abort(_('use --create to create a new queue'))
3464 3490 _setactive(name)
3465 3491
3466 3492 def mqphasedefaults(repo, roots):
3467 3493 """callback used to set mq changeset as secret when no phase data exists"""
3468 3494 if repo.mq.applied:
3469 3495 if repo.ui.configbool('mq', 'secret'):
3470 3496 mqphase = phases.secret
3471 3497 else:
3472 3498 mqphase = phases.draft
3473 3499 qbase = repo[repo.mq.applied[0].node]
3474 3500 roots[mqphase].add(qbase.node())
3475 3501 return roots
3476 3502
3477 3503 def reposetup(ui, repo):
3478 3504 class mqrepo(repo.__class__):
3479 3505 @localrepo.unfilteredpropertycache
3480 3506 def mq(self):
3481 3507 return queue(self.ui, self.baseui, self.path)
3482 3508
3483 3509 def invalidateall(self):
3484 3510 super(mqrepo, self).invalidateall()
3485 3511 if localrepo.hasunfilteredcache(self, 'mq'):
3486 3512 # recreate mq in case queue path was changed
3487 3513 delattr(self.unfiltered(), 'mq')
3488 3514
3489 3515 def abortifwdirpatched(self, errmsg, force=False):
3490 3516 if self.mq.applied and self.mq.checkapplied and not force:
3491 3517 parents = self.dirstate.parents()
3492 3518 patches = [s.node for s in self.mq.applied]
3493 3519 if parents[0] in patches or parents[1] in patches:
3494 3520 raise error.Abort(errmsg)
3495 3521
3496 3522 def commit(self, text="", user=None, date=None, match=None,
3497 3523 force=False, editor=False, extra=None):
3498 3524 if extra is None:
3499 3525 extra = {}
3500 3526 self.abortifwdirpatched(
3501 3527 _('cannot commit over an applied mq patch'),
3502 3528 force)
3503 3529
3504 3530 return super(mqrepo, self).commit(text, user, date, match, force,
3505 3531 editor, extra)
3506 3532
3507 3533 def checkpush(self, pushop):
3508 3534 if self.mq.applied and self.mq.checkapplied and not pushop.force:
3509 3535 outapplied = [e.node for e in self.mq.applied]
3510 3536 if pushop.revs:
3511 3537 # Assume applied patches have no non-patch descendants and
3512 3538 # are not on remote already. Filtering any changeset not
3513 3539 # pushed.
3514 3540 heads = set(pushop.revs)
3515 3541 for node in reversed(outapplied):
3516 3542 if node in heads:
3517 3543 break
3518 3544 else:
3519 3545 outapplied.pop()
3520 3546 # looking for pushed and shared changeset
3521 3547 for node in outapplied:
3522 3548 if self[node].phase() < phases.secret:
3523 3549 raise error.Abort(_('source has mq patches applied'))
3524 3550 # no non-secret patches pushed
3525 3551 super(mqrepo, self).checkpush(pushop)
3526 3552
3527 3553 def _findtags(self):
3528 3554 '''augment tags from base class with patch tags'''
3529 3555 result = super(mqrepo, self)._findtags()
3530 3556
3531 3557 q = self.mq
3532 3558 if not q.applied:
3533 3559 return result
3534 3560
3535 3561 mqtags = [(patch.node, patch.name) for patch in q.applied]
3536 3562
3537 3563 try:
3538 3564 # for now ignore filtering business
3539 3565 self.unfiltered().changelog.rev(mqtags[-1][0])
3540 3566 except error.LookupError:
3541 3567 self.ui.warn(_('mq status file refers to unknown node %s\n')
3542 3568 % short(mqtags[-1][0]))
3543 3569 return result
3544 3570
3545 3571 # do not add fake tags for filtered revisions
3546 3572 included = self.changelog.hasnode
3547 3573 mqtags = [mqt for mqt in mqtags if included(mqt[0])]
3548 3574 if not mqtags:
3549 3575 return result
3550 3576
3551 3577 mqtags.append((mqtags[-1][0], 'qtip'))
3552 3578 mqtags.append((mqtags[0][0], 'qbase'))
3553 3579 mqtags.append((self.changelog.parents(mqtags[0][0])[0], 'qparent'))
3554 3580 tags = result[0]
3555 3581 for patch in mqtags:
3556 3582 if patch[1] in tags:
3557 3583 self.ui.warn(_('tag %s overrides mq patch of the same '
3558 3584 'name\n') % patch[1])
3559 3585 else:
3560 3586 tags[patch[1]] = patch[0]
3561 3587
3562 3588 return result
3563 3589
3564 3590 if repo.local():
3565 3591 repo.__class__ = mqrepo
3566 3592
3567 3593 repo._phasedefaults.append(mqphasedefaults)
3568 3594
3569 3595 def mqimport(orig, ui, repo, *args, **kwargs):
3570 3596 if (util.safehasattr(repo, 'abortifwdirpatched')
3571 3597 and not kwargs.get(r'no_commit', False)):
3572 3598 repo.abortifwdirpatched(_('cannot import over an applied patch'),
3573 3599 kwargs.get(r'force'))
3574 3600 return orig(ui, repo, *args, **kwargs)
3575 3601
3576 3602 def mqinit(orig, ui, *args, **kwargs):
3577 3603 mq = kwargs.pop(r'mq', None)
3578 3604
3579 3605 if not mq:
3580 3606 return orig(ui, *args, **kwargs)
3581 3607
3582 3608 if args:
3583 3609 repopath = args[0]
3584 3610 if not hg.islocal(repopath):
3585 3611 raise error.Abort(_('only a local queue repository '
3586 3612 'may be initialized'))
3587 3613 else:
3588 3614 repopath = cmdutil.findrepo(encoding.getcwd())
3589 3615 if not repopath:
3590 3616 raise error.Abort(_('there is no Mercurial repository here '
3591 3617 '(.hg not found)'))
3592 3618 repo = hg.repository(ui, repopath)
3593 3619 return qinit(ui, repo, True)
3594 3620
3595 3621 def mqcommand(orig, ui, repo, *args, **kwargs):
3596 3622 """Add --mq option to operate on patch repository instead of main"""
3597 3623
3598 3624 # some commands do not like getting unknown options
3599 3625 mq = kwargs.pop(r'mq', None)
3600 3626
3601 3627 if not mq:
3602 3628 return orig(ui, repo, *args, **kwargs)
3603 3629
3604 3630 q = repo.mq
3605 3631 r = q.qrepo()
3606 3632 if not r:
3607 3633 raise error.Abort(_('no queue repository'))
3608 3634 return orig(r.ui, r, *args, **kwargs)
3609 3635
3610 3636 def summaryhook(ui, repo):
3611 3637 q = repo.mq
3612 3638 m = []
3613 3639 a, u = len(q.applied), len(q.unapplied(repo))
3614 3640 if a:
3615 3641 m.append(ui.label(_("%d applied"), 'qseries.applied') % a)
3616 3642 if u:
3617 3643 m.append(ui.label(_("%d unapplied"), 'qseries.unapplied') % u)
3618 3644 if m:
3619 3645 # i18n: column positioning for "hg summary"
3620 3646 ui.write(_("mq: %s\n") % ', '.join(m))
3621 3647 else:
3622 3648 # i18n: column positioning for "hg summary"
3623 3649 ui.note(_("mq: (empty queue)\n"))
3624 3650
3625 3651 revsetpredicate = registrar.revsetpredicate()
3626 3652
3627 3653 @revsetpredicate('mq()')
3628 3654 def revsetmq(repo, subset, x):
3629 3655 """Changesets managed by MQ.
3630 3656 """
3631 3657 revsetlang.getargs(x, 0, 0, _("mq takes no arguments"))
3632 3658 applied = set([repo[r.node].rev() for r in repo.mq.applied])
3633 3659 return smartset.baseset([r for r in subset if r in applied])
3634 3660
3635 3661 # tell hggettext to extract docstrings from these functions:
3636 3662 i18nfunctions = [revsetmq]
3637 3663
3638 3664 def extsetup(ui):
3639 3665 # Ensure mq wrappers are called first, regardless of extension load order by
3640 3666 # NOT wrapping in uisetup() and instead deferring to init stage two here.
3641 3667 mqopt = [('', 'mq', None, _("operate on patch repository"))]
3642 3668
3643 3669 extensions.wrapcommand(commands.table, 'import', mqimport)
3644 3670 cmdutil.summaryhooks.add('mq', summaryhook)
3645 3671
3646 3672 entry = extensions.wrapcommand(commands.table, 'init', mqinit)
3647 3673 entry[1].extend(mqopt)
3648 3674
3649 3675 def dotable(cmdtable):
3650 3676 for cmd, entry in cmdtable.iteritems():
3651 3677 cmd = cmdutil.parsealiases(cmd)[0]
3652 3678 func = entry[0]
3653 3679 if func.norepo:
3654 3680 continue
3655 3681 entry = extensions.wrapcommand(cmdtable, cmd, mqcommand)
3656 3682 entry[1].extend(mqopt)
3657 3683
3658 3684 dotable(commands.table)
3659 3685
3660 3686 for extname, extmodule in extensions.extensions():
3661 3687 if extmodule.__file__ != __file__:
3662 3688 dotable(getattr(extmodule, 'cmdtable', {}))
3663 3689
3664 3690 colortable = {'qguard.negative': 'red',
3665 3691 'qguard.positive': 'yellow',
3666 3692 'qguard.unguarded': 'green',
3667 3693 'qseries.applied': 'blue bold underline',
3668 3694 'qseries.guarded': 'black bold',
3669 3695 'qseries.missing': 'red bold',
3670 3696 'qseries.unapplied': 'black bold'}
@@ -1,835 +1,836 b''
1 1 # patchbomb.py - sending Mercurial changesets as patch emails
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
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 '''command to send changesets as (a series of) patch emails
9 9
10 10 The series is started off with a "[PATCH 0 of N]" introduction, which
11 11 describes the series as a whole.
12 12
13 13 Each patch email has a Subject line of "[PATCH M of N] ...", using the
14 14 first line of the changeset description as the subject text. The
15 15 message contains two or three body parts:
16 16
17 17 - The changeset description.
18 18 - [Optional] The result of running diffstat on the patch.
19 19 - The patch itself, as generated by :hg:`export`.
20 20
21 21 Each message refers to the first in the series using the In-Reply-To
22 22 and References headers, so they will show up as a sequence in threaded
23 23 mail and news readers, and in mail archives.
24 24
25 25 To configure other defaults, add a section like this to your
26 26 configuration file::
27 27
28 28 [email]
29 29 from = My Name <my@email>
30 30 to = recipient1, recipient2, ...
31 31 cc = cc1, cc2, ...
32 32 bcc = bcc1, bcc2, ...
33 33 reply-to = address1, address2, ...
34 34
35 35 Use ``[patchbomb]`` as configuration section name if you need to
36 36 override global ``[email]`` address settings.
37 37
38 38 Then you can use the :hg:`email` command to mail a series of
39 39 changesets as a patchbomb.
40 40
41 41 You can also either configure the method option in the email section
42 42 to be a sendmail compatible mailer or fill out the [smtp] section so
43 43 that the patchbomb extension can automatically send patchbombs
44 44 directly from the commandline. See the [email] and [smtp] sections in
45 45 hgrc(5) for details.
46 46
47 47 By default, :hg:`email` will prompt for a ``To`` or ``CC`` header if
48 48 you do not supply one via configuration or the command line. You can
49 49 override this to never prompt by configuring an empty value::
50 50
51 51 [email]
52 52 cc =
53 53
54 54 You can control the default inclusion of an introduction message with the
55 55 ``patchbomb.intro`` configuration option. The configuration is always
56 56 overwritten by command line flags like --intro and --desc::
57 57
58 58 [patchbomb]
59 59 intro=auto # include introduction message if more than 1 patch (default)
60 60 intro=never # never include an introduction message
61 61 intro=always # always include an introduction message
62 62
63 63 You can specify a template for flags to be added in subject prefixes. Flags
64 64 specified by --flag option are exported as ``{flags}`` keyword::
65 65
66 66 [patchbomb]
67 67 flagtemplate = "{separate(' ',
68 68 ifeq(branch, 'default', '', branch|upper),
69 69 flags)}"
70 70
71 71 You can set patchbomb to always ask for confirmation by setting
72 72 ``patchbomb.confirm`` to true.
73 73 '''
74 74 from __future__ import absolute_import
75 75
76 76 import email.encoders as emailencoders
77 77 import email.generator as emailgen
78 78 import email.mime.base as emimebase
79 79 import email.mime.multipart as emimemultipart
80 80 import email.utils as eutil
81 81 import errno
82 82 import os
83 83 import socket
84 84
85 85 from mercurial.i18n import _
86 86 from mercurial import (
87 87 cmdutil,
88 88 commands,
89 89 encoding,
90 90 error,
91 91 formatter,
92 92 hg,
93 93 mail,
94 94 node as nodemod,
95 95 patch,
96 96 pycompat,
97 97 registrar,
98 98 scmutil,
99 99 templater,
100 100 util,
101 101 )
102 102 from mercurial.utils import dateutil
103 103 stringio = util.stringio
104 104
105 105 cmdtable = {}
106 106 command = registrar.command(cmdtable)
107 107
108 108 configtable = {}
109 109 configitem = registrar.configitem(configtable)
110 110
111 111 configitem('patchbomb', 'bundletype',
112 112 default=None,
113 113 )
114 114 configitem('patchbomb', 'bcc',
115 115 default=None,
116 116 )
117 117 configitem('patchbomb', 'cc',
118 118 default=None,
119 119 )
120 120 configitem('patchbomb', 'confirm',
121 121 default=False,
122 122 )
123 123 configitem('patchbomb', 'flagtemplate',
124 124 default=None,
125 125 )
126 126 configitem('patchbomb', 'from',
127 127 default=None,
128 128 )
129 129 configitem('patchbomb', 'intro',
130 130 default='auto',
131 131 )
132 132 configitem('patchbomb', 'publicurl',
133 133 default=None,
134 134 )
135 135 configitem('patchbomb', 'reply-to',
136 136 default=None,
137 137 )
138 138 configitem('patchbomb', 'to',
139 139 default=None,
140 140 )
141 141
142 142 if pycompat.ispy3:
143 143 _bytesgenerator = emailgen.BytesGenerator
144 144 else:
145 145 _bytesgenerator = emailgen.Generator
146 146
147 147 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
148 148 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
149 149 # be specifying the version(s) of Mercurial they are tested with, or
150 150 # leave the attribute unspecified.
151 151 testedwith = 'ships-with-hg-core'
152 152
153 153 def _addpullheader(seq, ctx):
154 154 """Add a header pointing to a public URL where the changeset is available
155 155 """
156 156 repo = ctx.repo()
157 157 # experimental config: patchbomb.publicurl
158 158 # waiting for some logic that check that the changeset are available on the
159 159 # destination before patchbombing anything.
160 160 publicurl = repo.ui.config('patchbomb', 'publicurl')
161 161 if publicurl:
162 162 return ('Available At %s\n'
163 163 '# hg pull %s -r %s' % (publicurl, publicurl, ctx))
164 164 return None
165 165
166 166 def uisetup(ui):
167 167 cmdutil.extraexport.append('pullurl')
168 168 cmdutil.extraexportmap['pullurl'] = _addpullheader
169 169
170 170 def reposetup(ui, repo):
171 171 if not repo.local():
172 172 return
173 173 repo._wlockfreeprefix.add('last-email.txt')
174 174
175 175 def prompt(ui, prompt, default=None, rest=':'):
176 176 if default:
177 177 prompt += ' [%s]' % default
178 178 return ui.prompt(prompt + rest, default)
179 179
180 180 def introwanted(ui, opts, number):
181 181 '''is an introductory message apparently wanted?'''
182 182 introconfig = ui.config('patchbomb', 'intro')
183 183 if opts.get('intro') or opts.get('desc'):
184 184 intro = True
185 185 elif introconfig == 'always':
186 186 intro = True
187 187 elif introconfig == 'never':
188 188 intro = False
189 189 elif introconfig == 'auto':
190 190 intro = number > 1
191 191 else:
192 192 ui.write_err(_('warning: invalid patchbomb.intro value "%s"\n')
193 193 % introconfig)
194 194 ui.write_err(_('(should be one of always, never, auto)\n'))
195 195 intro = number > 1
196 196 return intro
197 197
198 198 def _formatflags(ui, repo, rev, flags):
199 199 """build flag string optionally by template"""
200 200 tmpl = ui.config('patchbomb', 'flagtemplate')
201 201 if not tmpl:
202 202 return ' '.join(flags)
203 203 out = util.stringio()
204 204 opts = {'template': templater.unquotestring(tmpl)}
205 205 with formatter.templateformatter(ui, out, 'patchbombflag', opts) as fm:
206 206 fm.startitem()
207 207 fm.context(ctx=repo[rev])
208 208 fm.write('flags', '%s', fm.formatlist(flags, name='flag'))
209 209 return out.getvalue()
210 210
211 211 def _formatprefix(ui, repo, rev, flags, idx, total, numbered):
212 212 """build prefix to patch subject"""
213 213 flag = _formatflags(ui, repo, rev, flags)
214 214 if flag:
215 215 flag = ' ' + flag
216 216
217 217 if not numbered:
218 218 return '[PATCH%s]' % flag
219 219 else:
220 220 tlen = len("%d" % total)
221 221 return '[PATCH %0*d of %d%s]' % (tlen, idx, total, flag)
222 222
223 223 def makepatch(ui, repo, rev, patchlines, opts, _charsets, idx, total, numbered,
224 224 patchname=None):
225 225
226 226 desc = []
227 227 node = None
228 228 body = ''
229 229
230 230 for line in patchlines:
231 231 if line.startswith('#'):
232 232 if line.startswith('# Node ID'):
233 233 node = line.split()[-1]
234 234 continue
235 235 if line.startswith('diff -r') or line.startswith('diff --git'):
236 236 break
237 237 desc.append(line)
238 238
239 239 if not patchname and not node:
240 240 raise ValueError
241 241
242 242 if opts.get('attach') and not opts.get('body'):
243 243 body = ('\n'.join(desc[1:]).strip() or
244 244 'Patch subject is complete summary.')
245 245 body += '\n\n\n'
246 246
247 247 if opts.get('plain'):
248 248 while patchlines and patchlines[0].startswith('# '):
249 249 patchlines.pop(0)
250 250 if patchlines:
251 251 patchlines.pop(0)
252 252 while patchlines and not patchlines[0].strip():
253 253 patchlines.pop(0)
254 254
255 255 ds = patch.diffstat(patchlines)
256 256 if opts.get('diffstat'):
257 257 body += ds + '\n\n'
258 258
259 259 addattachment = opts.get('attach') or opts.get('inline')
260 260 if not addattachment or opts.get('body'):
261 261 body += '\n'.join(patchlines)
262 262
263 263 if addattachment:
264 264 msg = emimemultipart.MIMEMultipart()
265 265 if body:
266 266 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
267 267 p = mail.mimetextpatch('\n'.join(patchlines), 'x-patch',
268 268 opts.get('test'))
269 269 binnode = nodemod.bin(node)
270 270 # if node is mq patch, it will have the patch file's name as a tag
271 271 if not patchname:
272 272 patchtags = [t for t in repo.nodetags(binnode)
273 273 if t.endswith('.patch') or t.endswith('.diff')]
274 274 if patchtags:
275 275 patchname = patchtags[0]
276 276 elif total > 1:
277 277 patchname = cmdutil.makefilename(repo[node], '%b-%n.patch',
278 278 seqno=idx, total=total)
279 279 else:
280 280 patchname = cmdutil.makefilename(repo[node], '%b.patch')
281 281 disposition = r'inline'
282 282 if opts.get('attach'):
283 283 disposition = r'attachment'
284 284 p[r'Content-Disposition'] = (
285 285 disposition + r'; filename=' + encoding.strfromlocal(patchname))
286 286 msg.attach(p)
287 287 else:
288 288 msg = mail.mimetextpatch(body, display=opts.get('test'))
289 289
290 290 prefix = _formatprefix(ui, repo, rev, opts.get('flag'), idx, total,
291 291 numbered)
292 292 subj = desc[0].strip().rstrip('. ')
293 293 if not numbered:
294 294 subj = ' '.join([prefix, opts.get('subject') or subj])
295 295 else:
296 296 subj = ' '.join([prefix, subj])
297 297 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
298 298 msg['X-Mercurial-Node'] = node
299 299 msg['X-Mercurial-Series-Index'] = '%i' % idx
300 300 msg['X-Mercurial-Series-Total'] = '%i' % total
301 301 return msg, subj, ds
302 302
303 303 def _getpatches(repo, revs, **opts):
304 304 """return a list of patches for a list of revisions
305 305
306 306 Each patch in the list is itself a list of lines.
307 307 """
308 308 ui = repo.ui
309 309 prev = repo['.'].rev()
310 310 for r in revs:
311 311 if r == prev and (repo[None].files() or repo[None].deleted()):
312 312 ui.warn(_('warning: working directory has '
313 313 'uncommitted changes\n'))
314 314 output = stringio()
315 315 cmdutil.exportfile(repo, [r], output,
316 316 opts=patch.difffeatureopts(ui, opts, git=True))
317 317 yield output.getvalue().split('\n')
318 318 def _getbundle(repo, dest, **opts):
319 319 """return a bundle containing changesets missing in "dest"
320 320
321 321 The `opts` keyword-arguments are the same as the one accepted by the
322 322 `bundle` command.
323 323
324 324 The bundle is a returned as a single in-memory binary blob.
325 325 """
326 326 ui = repo.ui
327 327 tmpdir = pycompat.mkdtemp(prefix='hg-email-bundle-')
328 328 tmpfn = os.path.join(tmpdir, 'bundle')
329 329 btype = ui.config('patchbomb', 'bundletype')
330 330 if btype:
331 331 opts[r'type'] = btype
332 332 try:
333 333 commands.bundle(ui, repo, tmpfn, dest, **opts)
334 334 return util.readfile(tmpfn)
335 335 finally:
336 336 try:
337 337 os.unlink(tmpfn)
338 338 except OSError:
339 339 pass
340 340 os.rmdir(tmpdir)
341 341
342 342 def _getdescription(repo, defaultbody, sender, **opts):
343 343 """obtain the body of the introduction message and return it
344 344
345 345 This is also used for the body of email with an attached bundle.
346 346
347 347 The body can be obtained either from the command line option or entered by
348 348 the user through the editor.
349 349 """
350 350 ui = repo.ui
351 351 if opts.get(r'desc'):
352 352 body = open(opts.get(r'desc')).read()
353 353 else:
354 354 ui.write(_('\nWrite the introductory message for the '
355 355 'patch series.\n\n'))
356 356 body = ui.edit(defaultbody, sender, repopath=repo.path,
357 357 action='patchbombbody')
358 358 # Save series description in case sendmail fails
359 359 msgfile = repo.vfs('last-email.txt', 'wb')
360 360 msgfile.write(body)
361 361 msgfile.close()
362 362 return body
363 363
364 364 def _getbundlemsgs(repo, sender, bundle, **opts):
365 365 """Get the full email for sending a given bundle
366 366
367 367 This function returns a list of "email" tuples (subject, content, None).
368 368 The list is always one message long in that case.
369 369 """
370 370 ui = repo.ui
371 371 _charsets = mail._charsets(ui)
372 372 subj = (opts.get(r'subject')
373 373 or prompt(ui, 'Subject:', 'A bundle for your repository'))
374 374
375 375 body = _getdescription(repo, '', sender, **opts)
376 376 msg = emimemultipart.MIMEMultipart()
377 377 if body:
378 378 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get(r'test')))
379 379 datapart = emimebase.MIMEBase(r'application', r'x-mercurial-bundle')
380 380 datapart.set_payload(bundle)
381 381 bundlename = '%s.hg' % opts.get(r'bundlename', 'bundle')
382 382 datapart.add_header(r'Content-Disposition', r'attachment',
383 383 filename=encoding.strfromlocal(bundlename))
384 384 emailencoders.encode_base64(datapart)
385 385 msg.attach(datapart)
386 386 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get(r'test'))
387 387 return [(msg, subj, None)]
388 388
389 389 def _makeintro(repo, sender, revs, patches, **opts):
390 390 """make an introduction email, asking the user for content if needed
391 391
392 392 email is returned as (subject, body, cumulative-diffstat)"""
393 393 ui = repo.ui
394 394 _charsets = mail._charsets(ui)
395 395
396 396 # use the last revision which is likely to be a bookmarked head
397 397 prefix = _formatprefix(ui, repo, revs.last(), opts.get(r'flag'),
398 398 0, len(patches), numbered=True)
399 399 subj = (opts.get(r'subject') or
400 400 prompt(ui, '(optional) Subject: ', rest=prefix, default=''))
401 401 if not subj:
402 402 return None # skip intro if the user doesn't bother
403 403
404 404 subj = prefix + ' ' + subj
405 405
406 406 body = ''
407 407 if opts.get(r'diffstat'):
408 408 # generate a cumulative diffstat of the whole patch series
409 409 diffstat = patch.diffstat(sum(patches, []))
410 410 body = '\n' + diffstat
411 411 else:
412 412 diffstat = None
413 413
414 414 body = _getdescription(repo, body, sender, **opts)
415 415 msg = mail.mimeencode(ui, body, _charsets, opts.get(r'test'))
416 416 msg['Subject'] = mail.headencode(ui, subj, _charsets,
417 417 opts.get(r'test'))
418 418 return (msg, subj, diffstat)
419 419
420 420 def _getpatchmsgs(repo, sender, revs, patchnames=None, **opts):
421 421 """return a list of emails from a list of patches
422 422
423 423 This involves introduction message creation if necessary.
424 424
425 425 This function returns a list of "email" tuples (subject, content, None).
426 426 """
427 427 bytesopts = pycompat.byteskwargs(opts)
428 428 ui = repo.ui
429 429 _charsets = mail._charsets(ui)
430 430 patches = list(_getpatches(repo, revs, **opts))
431 431 msgs = []
432 432
433 433 ui.write(_('this patch series consists of %d patches.\n\n')
434 434 % len(patches))
435 435
436 436 # build the intro message, or skip it if the user declines
437 437 if introwanted(ui, bytesopts, len(patches)):
438 438 msg = _makeintro(repo, sender, revs, patches, **opts)
439 439 if msg:
440 440 msgs.append(msg)
441 441
442 442 # are we going to send more than one message?
443 443 numbered = len(msgs) + len(patches) > 1
444 444
445 445 # now generate the actual patch messages
446 446 name = None
447 447 assert len(revs) == len(patches)
448 448 for i, (r, p) in enumerate(zip(revs, patches)):
449 449 if patchnames:
450 450 name = patchnames[i]
451 451 msg = makepatch(ui, repo, r, p, bytesopts, _charsets,
452 452 i + 1, len(patches), numbered, name)
453 453 msgs.append(msg)
454 454
455 455 return msgs
456 456
457 457 def _getoutgoing(repo, dest, revs):
458 458 '''Return the revisions present locally but not in dest'''
459 459 ui = repo.ui
460 460 url = ui.expandpath(dest or 'default-push', dest or 'default')
461 461 url = hg.parseurl(url)[0]
462 462 ui.status(_('comparing with %s\n') % util.hidepassword(url))
463 463
464 464 revs = [r for r in revs if r >= 0]
465 465 if not revs:
466 466 revs = [repo.changelog.tiprev()]
467 467 revs = repo.revs('outgoing(%s) and ::%ld', dest or '', revs)
468 468 if not revs:
469 469 ui.status(_("no changes found\n"))
470 470 return revs
471 471
472 472 def _msgid(node, timestamp):
473 473 hostname = encoding.strtolocal(socket.getfqdn())
474 474 hostname = encoding.environ.get('HGHOSTNAME', hostname)
475 475 return '<%s.%d@%s>' % (node, timestamp, hostname)
476 476
477 477 emailopts = [
478 478 ('', 'body', None, _('send patches as inline message text (default)')),
479 479 ('a', 'attach', None, _('send patches as attachments')),
480 480 ('i', 'inline', None, _('send patches as inline attachments')),
481 481 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
482 482 ('c', 'cc', [], _('email addresses of copy recipients')),
483 483 ('', 'confirm', None, _('ask for confirmation before sending')),
484 484 ('d', 'diffstat', None, _('add diffstat output to messages')),
485 485 ('', 'date', '', _('use the given date as the sending date')),
486 486 ('', 'desc', '', _('use the given file as the series description')),
487 487 ('f', 'from', '', _('email address of sender')),
488 488 ('n', 'test', None, _('print messages that would be sent')),
489 489 ('m', 'mbox', '', _('write messages to mbox file instead of sending them')),
490 490 ('', 'reply-to', [], _('email addresses replies should be sent to')),
491 491 ('s', 'subject', '', _('subject of first message (intro or single patch)')),
492 492 ('', 'in-reply-to', '', _('message identifier to reply to')),
493 493 ('', 'flag', [], _('flags to add in subject prefixes')),
494 494 ('t', 'to', [], _('email addresses of recipients'))]
495 495
496 496 @command('email',
497 497 [('g', 'git', None, _('use git extended diff format')),
498 498 ('', 'plain', None, _('omit hg patch header')),
499 499 ('o', 'outgoing', None,
500 500 _('send changes not found in the target repository')),
501 501 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
502 502 ('B', 'bookmark', '', _('send changes only reachable by given bookmark')),
503 503 ('', 'bundlename', 'bundle',
504 504 _('name of the bundle attachment file'), _('NAME')),
505 505 ('r', 'rev', [], _('a revision to send'), _('REV')),
506 506 ('', 'force', None, _('run even when remote repository is unrelated '
507 507 '(with -b/--bundle)')),
508 508 ('', 'base', [], _('a base changeset to specify instead of a destination '
509 509 '(with -b/--bundle)'), _('REV')),
510 510 ('', 'intro', None, _('send an introduction email for a single patch')),
511 511 ] + emailopts + cmdutil.remoteopts,
512 _('hg email [OPTION]... [DEST]...'))
512 _('hg email [OPTION]... [DEST]...'),
513 helpcategory=command.CATEGORY_IMPORT_EXPORT)
513 514 def email(ui, repo, *revs, **opts):
514 515 '''send changesets by email
515 516
516 517 By default, diffs are sent in the format generated by
517 518 :hg:`export`, one per message. The series starts with a "[PATCH 0
518 519 of N]" introduction, which describes the series as a whole.
519 520
520 521 Each patch email has a Subject line of "[PATCH M of N] ...", using
521 522 the first line of the changeset description as the subject text.
522 523 The message contains two or three parts. First, the changeset
523 524 description.
524 525
525 526 With the -d/--diffstat option, if the diffstat program is
526 527 installed, the result of running diffstat on the patch is inserted.
527 528
528 529 Finally, the patch itself, as generated by :hg:`export`.
529 530
530 531 With the -d/--diffstat or --confirm options, you will be presented
531 532 with a final summary of all messages and asked for confirmation before
532 533 the messages are sent.
533 534
534 535 By default the patch is included as text in the email body for
535 536 easy reviewing. Using the -a/--attach option will instead create
536 537 an attachment for the patch. With -i/--inline an inline attachment
537 538 will be created. You can include a patch both as text in the email
538 539 body and as a regular or an inline attachment by combining the
539 540 -a/--attach or -i/--inline with the --body option.
540 541
541 542 With -B/--bookmark changesets reachable by the given bookmark are
542 543 selected.
543 544
544 545 With -o/--outgoing, emails will be generated for patches not found
545 546 in the destination repository (or only those which are ancestors
546 547 of the specified revisions if any are provided)
547 548
548 549 With -b/--bundle, changesets are selected as for --outgoing, but a
549 550 single email containing a binary Mercurial bundle as an attachment
550 551 will be sent. Use the ``patchbomb.bundletype`` config option to
551 552 control the bundle type as with :hg:`bundle --type`.
552 553
553 554 With -m/--mbox, instead of previewing each patchbomb message in a
554 555 pager or sending the messages directly, it will create a UNIX
555 556 mailbox file with the patch emails. This mailbox file can be
556 557 previewed with any mail user agent which supports UNIX mbox
557 558 files.
558 559
559 560 With -n/--test, all steps will run, but mail will not be sent.
560 561 You will be prompted for an email recipient address, a subject and
561 562 an introductory message describing the patches of your patchbomb.
562 563 Then when all is done, patchbomb messages are displayed.
563 564
564 565 In case email sending fails, you will find a backup of your series
565 566 introductory message in ``.hg/last-email.txt``.
566 567
567 568 The default behavior of this command can be customized through
568 569 configuration. (See :hg:`help patchbomb` for details)
569 570
570 571 Examples::
571 572
572 573 hg email -r 3000 # send patch 3000 only
573 574 hg email -r 3000 -r 3001 # send patches 3000 and 3001
574 575 hg email -r 3000:3005 # send patches 3000 through 3005
575 576 hg email 3000 # send patch 3000 (deprecated)
576 577
577 578 hg email -o # send all patches not in default
578 579 hg email -o DEST # send all patches not in DEST
579 580 hg email -o -r 3000 # send all ancestors of 3000 not in default
580 581 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
581 582
582 583 hg email -B feature # send all ancestors of feature bookmark
583 584
584 585 hg email -b # send bundle of all patches not in default
585 586 hg email -b DEST # send bundle of all patches not in DEST
586 587 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
587 588 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
588 589
589 590 hg email -o -m mbox && # generate an mbox file...
590 591 mutt -R -f mbox # ... and view it with mutt
591 592 hg email -o -m mbox && # generate an mbox file ...
592 593 formail -s sendmail \\ # ... and use formail to send from the mbox
593 594 -bm -t < mbox # ... using sendmail
594 595
595 596 Before using this command, you will need to enable email in your
596 597 hgrc. See the [email] section in hgrc(5) for details.
597 598 '''
598 599 opts = pycompat.byteskwargs(opts)
599 600
600 601 _charsets = mail._charsets(ui)
601 602
602 603 bundle = opts.get('bundle')
603 604 date = opts.get('date')
604 605 mbox = opts.get('mbox')
605 606 outgoing = opts.get('outgoing')
606 607 rev = opts.get('rev')
607 608 bookmark = opts.get('bookmark')
608 609
609 610 if not (opts.get('test') or mbox):
610 611 # really sending
611 612 mail.validateconfig(ui)
612 613
613 614 if not (revs or rev or outgoing or bundle or bookmark):
614 615 raise error.Abort(_('specify at least one changeset with -B, -r or -o'))
615 616
616 617 if outgoing and bundle:
617 618 raise error.Abort(_("--outgoing mode always on with --bundle;"
618 619 " do not re-specify --outgoing"))
619 620 if rev and bookmark:
620 621 raise error.Abort(_("-r and -B are mutually exclusive"))
621 622
622 623 if outgoing or bundle:
623 624 if len(revs) > 1:
624 625 raise error.Abort(_("too many destinations"))
625 626 if revs:
626 627 dest = revs[0]
627 628 else:
628 629 dest = None
629 630 revs = []
630 631
631 632 if rev:
632 633 if revs:
633 634 raise error.Abort(_('use only one form to specify the revision'))
634 635 revs = rev
635 636 elif bookmark:
636 637 if bookmark not in repo._bookmarks:
637 638 raise error.Abort(_("bookmark '%s' not found") % bookmark)
638 639 revs = scmutil.bookmarkrevs(repo, bookmark)
639 640
640 641 revs = scmutil.revrange(repo, revs)
641 642 if outgoing:
642 643 revs = _getoutgoing(repo, dest, revs)
643 644 if bundle:
644 645 opts['revs'] = ["%d" % r for r in revs]
645 646
646 647 # check if revision exist on the public destination
647 648 publicurl = repo.ui.config('patchbomb', 'publicurl')
648 649 if publicurl:
649 650 repo.ui.debug('checking that revision exist in the public repo\n')
650 651 try:
651 652 publicpeer = hg.peer(repo, {}, publicurl)
652 653 except error.RepoError:
653 654 repo.ui.write_err(_('unable to access public repo: %s\n')
654 655 % publicurl)
655 656 raise
656 657 if not publicpeer.capable('known'):
657 658 repo.ui.debug('skipping existence checks: public repo too old\n')
658 659 else:
659 660 out = [repo[r] for r in revs]
660 661 known = publicpeer.known(h.node() for h in out)
661 662 missing = []
662 663 for idx, h in enumerate(out):
663 664 if not known[idx]:
664 665 missing.append(h)
665 666 if missing:
666 667 if len(missing) > 1:
667 668 msg = _('public "%s" is missing %s and %i others')
668 669 msg %= (publicurl, missing[0], len(missing) - 1)
669 670 else:
670 671 msg = _('public url %s is missing %s')
671 672 msg %= (publicurl, missing[0])
672 673 missingrevs = [ctx.rev() for ctx in missing]
673 674 revhint = ' '.join('-r %s' % h
674 675 for h in repo.set('heads(%ld)', missingrevs))
675 676 hint = _("use 'hg push %s %s'") % (publicurl, revhint)
676 677 raise error.Abort(msg, hint=hint)
677 678
678 679 # start
679 680 if date:
680 681 start_time = dateutil.parsedate(date)
681 682 else:
682 683 start_time = dateutil.makedate()
683 684
684 685 def genmsgid(id):
685 686 return _msgid(id[:20], int(start_time[0]))
686 687
687 688 # deprecated config: patchbomb.from
688 689 sender = (opts.get('from') or ui.config('email', 'from') or
689 690 ui.config('patchbomb', 'from') or
690 691 prompt(ui, 'From', ui.username()))
691 692
692 693 if bundle:
693 694 stropts = pycompat.strkwargs(opts)
694 695 bundledata = _getbundle(repo, dest, **stropts)
695 696 bundleopts = stropts.copy()
696 697 bundleopts.pop(r'bundle', None) # already processed
697 698 msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts)
698 699 else:
699 700 msgs = _getpatchmsgs(repo, sender, revs, **pycompat.strkwargs(opts))
700 701
701 702 showaddrs = []
702 703
703 704 def getaddrs(header, ask=False, default=None):
704 705 configkey = header.lower()
705 706 opt = header.replace('-', '_').lower()
706 707 addrs = opts.get(opt)
707 708 if addrs:
708 709 showaddrs.append('%s: %s' % (header, ', '.join(addrs)))
709 710 return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
710 711
711 712 # not on the command line: fallback to config and then maybe ask
712 713 addr = (ui.config('email', configkey) or
713 714 ui.config('patchbomb', configkey))
714 715 if not addr:
715 716 specified = (ui.hasconfig('email', configkey) or
716 717 ui.hasconfig('patchbomb', configkey))
717 718 if not specified and ask:
718 719 addr = prompt(ui, header, default=default)
719 720 if addr:
720 721 showaddrs.append('%s: %s' % (header, addr))
721 722 return mail.addrlistencode(ui, [addr], _charsets, opts.get('test'))
722 723 elif default:
723 724 return mail.addrlistencode(
724 725 ui, [default], _charsets, opts.get('test'))
725 726 return []
726 727
727 728 to = getaddrs('To', ask=True)
728 729 if not to:
729 730 # we can get here in non-interactive mode
730 731 raise error.Abort(_('no recipient addresses provided'))
731 732 cc = getaddrs('Cc', ask=True, default='')
732 733 bcc = getaddrs('Bcc')
733 734 replyto = getaddrs('Reply-To')
734 735
735 736 confirm = ui.configbool('patchbomb', 'confirm')
736 737 confirm |= bool(opts.get('diffstat') or opts.get('confirm'))
737 738
738 739 if confirm:
739 740 ui.write(_('\nFinal summary:\n\n'), label='patchbomb.finalsummary')
740 741 ui.write(('From: %s\n' % sender), label='patchbomb.from')
741 742 for addr in showaddrs:
742 743 ui.write('%s\n' % addr, label='patchbomb.to')
743 744 for m, subj, ds in msgs:
744 745 ui.write(('Subject: %s\n' % subj), label='patchbomb.subject')
745 746 if ds:
746 747 ui.write(ds, label='patchbomb.diffstats')
747 748 ui.write('\n')
748 749 if ui.promptchoice(_('are you sure you want to send (yn)?'
749 750 '$$ &Yes $$ &No')):
750 751 raise error.Abort(_('patchbomb canceled'))
751 752
752 753 ui.write('\n')
753 754
754 755 parent = opts.get('in_reply_to') or None
755 756 # angle brackets may be omitted, they're not semantically part of the msg-id
756 757 if parent is not None:
757 758 if not parent.startswith('<'):
758 759 parent = '<' + parent
759 760 if not parent.endswith('>'):
760 761 parent += '>'
761 762
762 763 sender_addr = eutil.parseaddr(encoding.strfromlocal(sender))[1]
763 764 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
764 765 sendmail = None
765 766 firstpatch = None
766 767 progress = ui.makeprogress(_('sending'), unit=_('emails'), total=len(msgs))
767 768 for i, (m, subj, ds) in enumerate(msgs):
768 769 try:
769 770 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
770 771 if not firstpatch:
771 772 firstpatch = m['Message-Id']
772 773 m['X-Mercurial-Series-Id'] = firstpatch
773 774 except TypeError:
774 775 m['Message-Id'] = genmsgid('patchbomb')
775 776 if parent:
776 777 m['In-Reply-To'] = parent
777 778 m['References'] = parent
778 779 if not parent or 'X-Mercurial-Node' not in m:
779 780 parent = m['Message-Id']
780 781
781 782 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
782 783 m['Date'] = eutil.formatdate(start_time[0], localtime=True)
783 784
784 785 start_time = (start_time[0] + 1, start_time[1])
785 786 m['From'] = sender
786 787 m['To'] = ', '.join(to)
787 788 if cc:
788 789 m['Cc'] = ', '.join(cc)
789 790 if bcc:
790 791 m['Bcc'] = ', '.join(bcc)
791 792 if replyto:
792 793 m['Reply-To'] = ', '.join(replyto)
793 794 # Fix up all headers to be native strings.
794 795 # TODO(durin42): this should probably be cleaned up above in the future.
795 796 if pycompat.ispy3:
796 797 for hdr, val in list(m.items()):
797 798 change = False
798 799 if isinstance(hdr, bytes):
799 800 del m[hdr]
800 801 hdr = pycompat.strurl(hdr)
801 802 change = True
802 803 if isinstance(val, bytes):
803 804 val = pycompat.strurl(val)
804 805 if not change:
805 806 # prevent duplicate headers
806 807 del m[hdr]
807 808 change = True
808 809 if change:
809 810 m[hdr] = val
810 811 if opts.get('test'):
811 812 ui.status(_('displaying '), subj, ' ...\n')
812 813 ui.pager('email')
813 814 generator = _bytesgenerator(ui, mangle_from_=False)
814 815 try:
815 816 generator.flatten(m, 0)
816 817 ui.write('\n')
817 818 except IOError as inst:
818 819 if inst.errno != errno.EPIPE:
819 820 raise
820 821 else:
821 822 if not sendmail:
822 823 sendmail = mail.connect(ui, mbox=mbox)
823 824 ui.status(_('sending '), subj, ' ...\n')
824 825 progress.update(i, item=subj)
825 826 if not mbox:
826 827 # Exim does not remove the Bcc field
827 828 del m['Bcc']
828 829 fp = stringio()
829 830 generator = _bytesgenerator(fp, mangle_from_=False)
830 831 generator.flatten(m, 0)
831 832 alldests = to + bcc + cc
832 833 alldests = [encoding.strfromlocal(d) for d in alldests]
833 834 sendmail(sender_addr, alldests, fp.getvalue())
834 835
835 836 progress.complete()
@@ -1,110 +1,111 b''
1 1 # Copyright (C) 2006 - Marco Barisione <marco@barisione.org>
2 2 #
3 3 # This is a small extension for Mercurial (https://mercurial-scm.org/)
4 4 # that removes files not known to mercurial
5 5 #
6 6 # This program was inspired by the "cvspurge" script contained in CVS
7 7 # utilities (http://www.red-bean.com/cvsutils/).
8 8 #
9 9 # For help on the usage of "hg purge" use:
10 10 # hg help purge
11 11 #
12 12 # This program is free software; you can redistribute it and/or modify
13 13 # it under the terms of the GNU General Public License as published by
14 14 # the Free Software Foundation; either version 2 of the License, or
15 15 # (at your option) any later version.
16 16 #
17 17 # This program is distributed in the hope that it will be useful,
18 18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 20 # GNU General Public License for more details.
21 21 #
22 22 # You should have received a copy of the GNU General Public License
23 23 # along with this program; if not, see <http://www.gnu.org/licenses/>.
24 24
25 25 '''command to delete untracked files from the working directory'''
26 26 from __future__ import absolute_import
27 27
28 28 from mercurial.i18n import _
29 29 from mercurial import (
30 30 cmdutil,
31 31 merge as mergemod,
32 32 pycompat,
33 33 registrar,
34 34 scmutil,
35 35 )
36 36
37 37 cmdtable = {}
38 38 command = registrar.command(cmdtable)
39 39 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
40 40 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
41 41 # be specifying the version(s) of Mercurial they are tested with, or
42 42 # leave the attribute unspecified.
43 43 testedwith = 'ships-with-hg-core'
44 44
45 45 @command('purge|clean',
46 46 [('a', 'abort-on-err', None, _('abort if an error occurs')),
47 47 ('', 'all', None, _('purge ignored files too')),
48 48 ('', 'dirs', None, _('purge empty directories')),
49 49 ('', 'files', None, _('purge files')),
50 50 ('p', 'print', None, _('print filenames instead of deleting them')),
51 51 ('0', 'print0', None, _('end filenames with NUL, for use with xargs'
52 52 ' (implies -p/--print)')),
53 53 ] + cmdutil.walkopts,
54 _('hg purge [OPTION]... [DIR]...'))
54 _('hg purge [OPTION]... [DIR]...'),
55 helpcategory=command.CATEGORY_MAINTENANCE)
55 56 def purge(ui, repo, *dirs, **opts):
56 57 '''removes files not tracked by Mercurial
57 58
58 59 Delete files not known to Mercurial. This is useful to test local
59 60 and uncommitted changes in an otherwise-clean source tree.
60 61
61 62 This means that purge will delete the following by default:
62 63
63 64 - Unknown files: files marked with "?" by :hg:`status`
64 65 - Empty directories: in fact Mercurial ignores directories unless
65 66 they contain files under source control management
66 67
67 68 But it will leave untouched:
68 69
69 70 - Modified and unmodified tracked files
70 71 - Ignored files (unless --all is specified)
71 72 - New files added to the repository (with :hg:`add`)
72 73
73 74 The --files and --dirs options can be used to direct purge to delete
74 75 only files, only directories, or both. If neither option is given,
75 76 both will be deleted.
76 77
77 78 If directories are given on the command line, only files in these
78 79 directories are considered.
79 80
80 81 Be careful with purge, as you could irreversibly delete some files
81 82 you forgot to add to the repository. If you only want to print the
82 83 list of files that this program would delete, use the --print
83 84 option.
84 85 '''
85 86 opts = pycompat.byteskwargs(opts)
86 87
87 88 act = not opts.get('print')
88 89 eol = '\n'
89 90 if opts.get('print0'):
90 91 eol = '\0'
91 92 act = False # --print0 implies --print
92 93
93 94 removefiles = opts.get('files')
94 95 removedirs = opts.get('dirs')
95 96
96 97 if not removefiles and not removedirs:
97 98 removefiles = True
98 99 removedirs = True
99 100
100 101 match = scmutil.match(repo[None], dirs, opts)
101 102
102 103 paths = mergemod.purge(
103 104 repo, match, ignored=opts.get('all', False),
104 105 removeemptydirs=removedirs, removefiles=removefiles,
105 106 abortonerror=opts.get('abort_on_err'),
106 107 noop=not act)
107 108
108 109 for path in paths:
109 110 if not act:
110 111 ui.write('%s%s' % (path, eol))
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now