##// END OF EJS Templates
extensions: document that `testedwith = 'internal'` is special...
Augie Fackler -
r25186:80c5b266 default
parent child Browse files
Show More

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

@@ -1,491 +1,495 b''
1 1 # synthrepo.py - repo synthesis
2 2 #
3 3 # Copyright 2012 Facebook
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 '''synthesize structurally interesting change history
9 9
10 10 This extension is useful for creating a repository with properties
11 11 that are statistically similar to an existing repository. During
12 12 analysis, a simple probability table is constructed from the history
13 13 of an existing repository. During synthesis, these properties are
14 14 reconstructed.
15 15
16 16 Properties that are analyzed and synthesized include the following:
17 17
18 18 - Lines added or removed when an existing file is modified
19 19 - Number and sizes of files added
20 20 - Number of files removed
21 21 - Line lengths
22 22 - Topological distance to parent changeset(s)
23 23 - Probability of a commit being a merge
24 24 - Probability of a newly added file being added to a new directory
25 25 - Interarrival time, and time zone, of commits
26 26 - Number of files in each directory
27 27
28 28 A few obvious properties that are not currently handled realistically:
29 29
30 30 - Merges are treated as regular commits with two parents, which is not
31 31 realistic
32 32 - Modifications are not treated as operations on hunks of lines, but
33 33 as insertions and deletions of randomly chosen single lines
34 34 - Committer ID (always random)
35 35 - Executability of files
36 36 - Symlinks and binary files are ignored
37 37 '''
38 38
39 39 import bisect, collections, itertools, json, os, random, time, sys
40 40 from mercurial import cmdutil, context, patch, scmutil, util, hg
41 41 from mercurial.i18n import _
42 42 from mercurial.node import nullrev, nullid, short
43 43
44 # Note for extension authors: ONLY specify testedwith = 'internal' for
45 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
46 # be specifying the version(s) of Mercurial they are tested with, or
47 # leave the attribute unspecified.
44 48 testedwith = 'internal'
45 49
46 50 cmdtable = {}
47 51 command = cmdutil.command(cmdtable)
48 52
49 53 newfile = set(('new fi', 'rename', 'copy f', 'copy t'))
50 54
51 55 def zerodict():
52 56 return collections.defaultdict(lambda: 0)
53 57
54 58 def roundto(x, k):
55 59 if x > k * 2:
56 60 return int(round(x / float(k)) * k)
57 61 return int(round(x))
58 62
59 63 def parsegitdiff(lines):
60 64 filename, mar, lineadd, lineremove = None, None, zerodict(), 0
61 65 binary = False
62 66 for line in lines:
63 67 start = line[:6]
64 68 if start == 'diff -':
65 69 if filename:
66 70 yield filename, mar, lineadd, lineremove, binary
67 71 mar, lineadd, lineremove, binary = 'm', zerodict(), 0, False
68 72 filename = patch.gitre.match(line).group(1)
69 73 elif start in newfile:
70 74 mar = 'a'
71 75 elif start == 'GIT bi':
72 76 binary = True
73 77 elif start == 'delete':
74 78 mar = 'r'
75 79 elif start:
76 80 s = start[0]
77 81 if s == '-' and not line.startswith('--- '):
78 82 lineremove += 1
79 83 elif s == '+' and not line.startswith('+++ '):
80 84 lineadd[roundto(len(line) - 1, 5)] += 1
81 85 if filename:
82 86 yield filename, mar, lineadd, lineremove, binary
83 87
84 88 @command('analyze',
85 89 [('o', 'output', '', _('write output to given file'), _('FILE')),
86 90 ('r', 'rev', [], _('analyze specified revisions'), _('REV'))],
87 91 _('hg analyze'), optionalrepo=True)
88 92 def analyze(ui, repo, *revs, **opts):
89 93 '''create a simple model of a repository to use for later synthesis
90 94
91 95 This command examines every changeset in the given range (or all
92 96 of history if none are specified) and creates a simple statistical
93 97 model of the history of the repository. It also measures the directory
94 98 structure of the repository as checked out.
95 99
96 100 The model is written out to a JSON file, and can be used by
97 101 :hg:`synthesize` to create or augment a repository with synthetic
98 102 commits that have a structure that is statistically similar to the
99 103 analyzed repository.
100 104 '''
101 105 root = repo.root
102 106 if not root.endswith(os.path.sep):
103 107 root += os.path.sep
104 108
105 109 revs = list(revs)
106 110 revs.extend(opts['rev'])
107 111 if not revs:
108 112 revs = [':']
109 113
110 114 output = opts['output']
111 115 if not output:
112 116 output = os.path.basename(root) + '.json'
113 117
114 118 if output == '-':
115 119 fp = sys.stdout
116 120 else:
117 121 fp = open(output, 'w')
118 122
119 123 # Always obtain file counts of each directory in the given root directory.
120 124 def onerror(e):
121 125 ui.warn(_('error walking directory structure: %s\n') % e)
122 126
123 127 dirs = {}
124 128 rootprefixlen = len(root)
125 129 for dirpath, dirnames, filenames in os.walk(root, onerror=onerror):
126 130 dirpathfromroot = dirpath[rootprefixlen:]
127 131 dirs[dirpathfromroot] = len(filenames)
128 132 if '.hg' in dirnames:
129 133 dirnames.remove('.hg')
130 134
131 135 lineschanged = zerodict()
132 136 children = zerodict()
133 137 p1distance = zerodict()
134 138 p2distance = zerodict()
135 139 linesinfilesadded = zerodict()
136 140 fileschanged = zerodict()
137 141 filesadded = zerodict()
138 142 filesremoved = zerodict()
139 143 linelengths = zerodict()
140 144 interarrival = zerodict()
141 145 parents = zerodict()
142 146 dirsadded = zerodict()
143 147 tzoffset = zerodict()
144 148
145 149 # If a mercurial repo is available, also model the commit history.
146 150 if repo:
147 151 revs = scmutil.revrange(repo, revs)
148 152 revs.sort()
149 153
150 154 progress = ui.progress
151 155 _analyzing = _('analyzing')
152 156 _changesets = _('changesets')
153 157 _total = len(revs)
154 158
155 159 for i, rev in enumerate(revs):
156 160 progress(_analyzing, i, unit=_changesets, total=_total)
157 161 ctx = repo[rev]
158 162 pl = ctx.parents()
159 163 pctx = pl[0]
160 164 prev = pctx.rev()
161 165 children[prev] += 1
162 166 p1distance[rev - prev] += 1
163 167 parents[len(pl)] += 1
164 168 tzoffset[ctx.date()[1]] += 1
165 169 if len(pl) > 1:
166 170 p2distance[rev - pl[1].rev()] += 1
167 171 if prev == rev - 1:
168 172 lastctx = pctx
169 173 else:
170 174 lastctx = repo[rev - 1]
171 175 if lastctx.rev() != nullrev:
172 176 timedelta = ctx.date()[0] - lastctx.date()[0]
173 177 interarrival[roundto(timedelta, 300)] += 1
174 178 diff = sum((d.splitlines() for d in ctx.diff(pctx, git=True)), [])
175 179 fileadds, diradds, fileremoves, filechanges = 0, 0, 0, 0
176 180 for filename, mar, lineadd, lineremove, isbin in parsegitdiff(diff):
177 181 if isbin:
178 182 continue
179 183 added = sum(lineadd.itervalues(), 0)
180 184 if mar == 'm':
181 185 if added and lineremove:
182 186 lineschanged[roundto(added, 5),
183 187 roundto(lineremove, 5)] += 1
184 188 filechanges += 1
185 189 elif mar == 'a':
186 190 fileadds += 1
187 191 if '/' in filename:
188 192 filedir = filename.rsplit('/', 1)[0]
189 193 if filedir not in pctx.dirs():
190 194 diradds += 1
191 195 linesinfilesadded[roundto(added, 5)] += 1
192 196 elif mar == 'r':
193 197 fileremoves += 1
194 198 for length, count in lineadd.iteritems():
195 199 linelengths[length] += count
196 200 fileschanged[filechanges] += 1
197 201 filesadded[fileadds] += 1
198 202 dirsadded[diradds] += 1
199 203 filesremoved[fileremoves] += 1
200 204
201 205 invchildren = zerodict()
202 206
203 207 for rev, count in children.iteritems():
204 208 invchildren[count] += 1
205 209
206 210 if output != '-':
207 211 ui.status(_('writing output to %s\n') % output)
208 212
209 213 def pronk(d):
210 214 return sorted(d.iteritems(), key=lambda x: x[1], reverse=True)
211 215
212 216 json.dump({'revs': len(revs),
213 217 'initdirs': pronk(dirs),
214 218 'lineschanged': pronk(lineschanged),
215 219 'children': pronk(invchildren),
216 220 'fileschanged': pronk(fileschanged),
217 221 'filesadded': pronk(filesadded),
218 222 'linesinfilesadded': pronk(linesinfilesadded),
219 223 'dirsadded': pronk(dirsadded),
220 224 'filesremoved': pronk(filesremoved),
221 225 'linelengths': pronk(linelengths),
222 226 'parents': pronk(parents),
223 227 'p1distance': pronk(p1distance),
224 228 'p2distance': pronk(p2distance),
225 229 'interarrival': pronk(interarrival),
226 230 'tzoffset': pronk(tzoffset),
227 231 },
228 232 fp)
229 233 fp.close()
230 234
231 235 @command('synthesize',
232 236 [('c', 'count', 0, _('create given number of commits'), _('COUNT')),
233 237 ('', 'dict', '', _('path to a dictionary of words'), _('FILE')),
234 238 ('', 'initfiles', 0, _('initial file count to create'), _('COUNT'))],
235 239 _('hg synthesize [OPTION].. DESCFILE'))
236 240 def synthesize(ui, repo, descpath, **opts):
237 241 '''synthesize commits based on a model of an existing repository
238 242
239 243 The model must have been generated by :hg:`analyze`. Commits will
240 244 be generated randomly according to the probabilities described in
241 245 the model. If --initfiles is set, the repository will be seeded with
242 246 the given number files following the modeled repository's directory
243 247 structure.
244 248
245 249 When synthesizing new content, commit descriptions, and user
246 250 names, words will be chosen randomly from a dictionary that is
247 251 presumed to contain one word per line. Use --dict to specify the
248 252 path to an alternate dictionary to use.
249 253 '''
250 254 try:
251 255 fp = hg.openpath(ui, descpath)
252 256 except Exception, err:
253 257 raise util.Abort('%s: %s' % (descpath, err[0].strerror))
254 258 desc = json.load(fp)
255 259 fp.close()
256 260
257 261 def cdf(l):
258 262 if not l:
259 263 return [], []
260 264 vals, probs = zip(*sorted(l, key=lambda x: x[1], reverse=True))
261 265 t = float(sum(probs, 0))
262 266 s, cdfs = 0, []
263 267 for v in probs:
264 268 s += v
265 269 cdfs.append(s / t)
266 270 return vals, cdfs
267 271
268 272 lineschanged = cdf(desc['lineschanged'])
269 273 fileschanged = cdf(desc['fileschanged'])
270 274 filesadded = cdf(desc['filesadded'])
271 275 dirsadded = cdf(desc['dirsadded'])
272 276 filesremoved = cdf(desc['filesremoved'])
273 277 linelengths = cdf(desc['linelengths'])
274 278 parents = cdf(desc['parents'])
275 279 p1distance = cdf(desc['p1distance'])
276 280 p2distance = cdf(desc['p2distance'])
277 281 interarrival = cdf(desc['interarrival'])
278 282 linesinfilesadded = cdf(desc['linesinfilesadded'])
279 283 tzoffset = cdf(desc['tzoffset'])
280 284
281 285 dictfile = opts.get('dict') or '/usr/share/dict/words'
282 286 try:
283 287 fp = open(dictfile, 'rU')
284 288 except IOError, err:
285 289 raise util.Abort('%s: %s' % (dictfile, err.strerror))
286 290 words = fp.read().splitlines()
287 291 fp.close()
288 292
289 293 initdirs = {}
290 294 if desc['initdirs']:
291 295 for k, v in desc['initdirs']:
292 296 initdirs[k.encode('utf-8').replace('.hg', '_hg')] = v
293 297 initdirs = renamedirs(initdirs, words)
294 298 initdirscdf = cdf(initdirs)
295 299
296 300 def pick(cdf):
297 301 return cdf[0][bisect.bisect_left(cdf[1], random.random())]
298 302
299 303 def pickpath():
300 304 return os.path.join(pick(initdirscdf), random.choice(words))
301 305
302 306 def makeline(minimum=0):
303 307 total = max(minimum, pick(linelengths))
304 308 c, l = 0, []
305 309 while c < total:
306 310 w = random.choice(words)
307 311 c += len(w) + 1
308 312 l.append(w)
309 313 return ' '.join(l)
310 314
311 315 wlock = repo.wlock()
312 316 lock = repo.lock()
313 317
314 318 nevertouch = set(('.hgsub', '.hgignore', '.hgtags'))
315 319
316 320 progress = ui.progress
317 321 _synthesizing = _('synthesizing')
318 322 _files = _('initial files')
319 323 _changesets = _('changesets')
320 324
321 325 # Synthesize a single initial revision adding files to the repo according
322 326 # to the modeled directory structure.
323 327 initcount = int(opts['initfiles'])
324 328 if initcount and initdirs:
325 329 pctx = repo[None].parents()[0]
326 330 dirs = set(pctx.dirs())
327 331 files = {}
328 332
329 333 def validpath(path):
330 334 # Don't pick filenames which are already directory names.
331 335 if path in dirs:
332 336 return False
333 337 # Don't pick directories which were used as file names.
334 338 while path:
335 339 if path in files:
336 340 return False
337 341 path = os.path.dirname(path)
338 342 return True
339 343
340 344 for i in xrange(0, initcount):
341 345 ui.progress(_synthesizing, i, unit=_files, total=initcount)
342 346
343 347 path = pickpath()
344 348 while not validpath(path):
345 349 path = pickpath()
346 350 data = '%s contents\n' % path
347 351 files[path] = context.memfilectx(repo, path, data)
348 352 dir = os.path.dirname(path)
349 353 while dir and dir not in dirs:
350 354 dirs.add(dir)
351 355 dir = os.path.dirname(dir)
352 356
353 357 def filectxfn(repo, memctx, path):
354 358 return files[path]
355 359
356 360 ui.progress(_synthesizing, None)
357 361 message = 'synthesized wide repo with %d files' % (len(files),)
358 362 mc = context.memctx(repo, [pctx.node(), nullid], message,
359 363 files.iterkeys(), filectxfn, ui.username(),
360 364 '%d %d' % util.makedate())
361 365 initnode = mc.commit()
362 366 if ui.debugflag:
363 367 hexfn = hex
364 368 else:
365 369 hexfn = short
366 370 ui.status(_('added commit %s with %d files\n')
367 371 % (hexfn(initnode), len(files)))
368 372
369 373 # Synthesize incremental revisions to the repository, adding repo depth.
370 374 count = int(opts['count'])
371 375 heads = set(map(repo.changelog.rev, repo.heads()))
372 376 for i in xrange(count):
373 377 progress(_synthesizing, i, unit=_changesets, total=count)
374 378
375 379 node = repo.changelog.node
376 380 revs = len(repo)
377 381
378 382 def pickhead(heads, distance):
379 383 if heads:
380 384 lheads = sorted(heads)
381 385 rev = revs - min(pick(distance), revs)
382 386 if rev < lheads[-1]:
383 387 rev = lheads[bisect.bisect_left(lheads, rev)]
384 388 else:
385 389 rev = lheads[-1]
386 390 return rev, node(rev)
387 391 return nullrev, nullid
388 392
389 393 r1 = revs - min(pick(p1distance), revs)
390 394 p1 = node(r1)
391 395
392 396 # the number of heads will grow without bound if we use a pure
393 397 # model, so artificially constrain their proliferation
394 398 toomanyheads = len(heads) > random.randint(1, 20)
395 399 if p2distance[0] and (pick(parents) == 2 or toomanyheads):
396 400 r2, p2 = pickhead(heads.difference([r1]), p2distance)
397 401 else:
398 402 r2, p2 = nullrev, nullid
399 403
400 404 pl = [p1, p2]
401 405 pctx = repo[r1]
402 406 mf = pctx.manifest()
403 407 mfk = mf.keys()
404 408 changes = {}
405 409 if mfk:
406 410 for __ in xrange(pick(fileschanged)):
407 411 for __ in xrange(10):
408 412 fctx = pctx.filectx(random.choice(mfk))
409 413 path = fctx.path()
410 414 if not (path in nevertouch or fctx.isbinary() or
411 415 'l' in fctx.flags()):
412 416 break
413 417 lines = fctx.data().splitlines()
414 418 add, remove = pick(lineschanged)
415 419 for __ in xrange(remove):
416 420 if not lines:
417 421 break
418 422 del lines[random.randrange(0, len(lines))]
419 423 for __ in xrange(add):
420 424 lines.insert(random.randint(0, len(lines)), makeline())
421 425 path = fctx.path()
422 426 changes[path] = context.memfilectx(repo, path,
423 427 '\n'.join(lines) + '\n')
424 428 for __ in xrange(pick(filesremoved)):
425 429 path = random.choice(mfk)
426 430 for __ in xrange(10):
427 431 path = random.choice(mfk)
428 432 if path not in changes:
429 433 changes[path] = None
430 434 break
431 435 if filesadded:
432 436 dirs = list(pctx.dirs())
433 437 dirs.insert(0, '')
434 438 for __ in xrange(pick(filesadded)):
435 439 pathstr = ''
436 440 while pathstr in dirs:
437 441 path = [random.choice(dirs)]
438 442 if pick(dirsadded):
439 443 path.append(random.choice(words))
440 444 path.append(random.choice(words))
441 445 pathstr = '/'.join(filter(None, path))
442 446 data = '\n'.join(makeline()
443 447 for __ in xrange(pick(linesinfilesadded))) + '\n'
444 448 changes[pathstr] = context.memfilectx(repo, pathstr, data)
445 449 def filectxfn(repo, memctx, path):
446 450 return changes[path]
447 451 if not changes:
448 452 continue
449 453 if revs:
450 454 date = repo['tip'].date()[0] + pick(interarrival)
451 455 else:
452 456 date = time.time() - (86400 * count)
453 457 # dates in mercurial must be positive, fit in 32-bit signed integers.
454 458 date = min(0x7fffffff, max(0, date))
455 459 user = random.choice(words) + '@' + random.choice(words)
456 460 mc = context.memctx(repo, pl, makeline(minimum=2),
457 461 sorted(changes.iterkeys()),
458 462 filectxfn, user, '%d %d' % (date, pick(tzoffset)))
459 463 newnode = mc.commit()
460 464 heads.add(repo.changelog.rev(newnode))
461 465 heads.discard(r1)
462 466 heads.discard(r2)
463 467
464 468 lock.release()
465 469 wlock.release()
466 470
467 471 def renamedirs(dirs, words):
468 472 '''Randomly rename the directory names in the per-dir file count dict.'''
469 473 wordgen = itertools.cycle(words)
470 474 replacements = {'': ''}
471 475 def rename(dirpath):
472 476 '''Recursively rename the directory and all path prefixes.
473 477
474 478 The mapping from path to renamed path is stored for all path prefixes
475 479 as in dynamic programming, ensuring linear runtime and consistent
476 480 renaming regardless of iteration order through the model.
477 481 '''
478 482 if dirpath in replacements:
479 483 return replacements[dirpath]
480 484 head, _ = os.path.split(dirpath)
481 485 if head:
482 486 head = rename(head)
483 487 else:
484 488 head = ''
485 489 renamed = os.path.join(head, wordgen.next())
486 490 replacements[dirpath] = renamed
487 491 return renamed
488 492 result = []
489 493 for dirpath, count in dirs.iteritems():
490 494 result.append([rename(dirpath.lstrip(os.sep)), count])
491 495 return result
@@ -1,316 +1,320 b''
1 1 # acl.py - changeset access control 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 '''hooks for controlling repository access
9 9
10 10 This hook makes it possible to allow or deny write access to given
11 11 branches and paths of a repository when receiving incoming changesets
12 12 via pretxnchangegroup and pretxncommit.
13 13
14 14 The authorization is matched based on the local user name on the
15 15 system where the hook runs, and not the committer of the original
16 16 changeset (since the latter is merely informative).
17 17
18 18 The acl hook is best used along with a restricted shell like hgsh,
19 19 preventing authenticating users from doing anything other than pushing
20 20 or pulling. The hook is not safe to use if users have interactive
21 21 shell access, as they can then disable the hook. Nor is it safe if
22 22 remote users share an account, because then there is no way to
23 23 distinguish them.
24 24
25 25 The order in which access checks are performed is:
26 26
27 27 1) Deny list for branches (section ``acl.deny.branches``)
28 28 2) Allow list for branches (section ``acl.allow.branches``)
29 29 3) Deny list for paths (section ``acl.deny``)
30 30 4) Allow list for paths (section ``acl.allow``)
31 31
32 32 The allow and deny sections take key-value pairs.
33 33
34 34 Branch-based Access Control
35 35 ---------------------------
36 36
37 37 Use the ``acl.deny.branches`` and ``acl.allow.branches`` sections to
38 38 have branch-based access control. Keys in these sections can be
39 39 either:
40 40
41 41 - a branch name, or
42 42 - an asterisk, to match any branch;
43 43
44 44 The corresponding values can be either:
45 45
46 46 - a comma-separated list containing users and groups, or
47 47 - an asterisk, to match anyone;
48 48
49 49 You can add the "!" prefix to a user or group name to invert the sense
50 50 of the match.
51 51
52 52 Path-based Access Control
53 53 -------------------------
54 54
55 55 Use the ``acl.deny`` and ``acl.allow`` sections to have path-based
56 56 access control. Keys in these sections accept a subtree pattern (with
57 57 a glob syntax by default). The corresponding values follow the same
58 58 syntax as the other sections above.
59 59
60 60 Groups
61 61 ------
62 62
63 63 Group names must be prefixed with an ``@`` symbol. Specifying a group
64 64 name has the same effect as specifying all the users in that group.
65 65
66 66 You can define group members in the ``acl.groups`` section.
67 67 If a group name is not defined there, and Mercurial is running under
68 68 a Unix-like system, the list of users will be taken from the OS.
69 69 Otherwise, an exception will be raised.
70 70
71 71 Example Configuration
72 72 ---------------------
73 73
74 74 ::
75 75
76 76 [hooks]
77 77
78 78 # Use this if you want to check access restrictions at commit time
79 79 pretxncommit.acl = python:hgext.acl.hook
80 80
81 81 # Use this if you want to check access restrictions for pull, push,
82 82 # bundle and serve.
83 83 pretxnchangegroup.acl = python:hgext.acl.hook
84 84
85 85 [acl]
86 86 # Allow or deny access for incoming changes only if their source is
87 87 # listed here, let them pass otherwise. Source is "serve" for all
88 88 # remote access (http or ssh), "push", "pull" or "bundle" when the
89 89 # related commands are run locally.
90 90 # Default: serve
91 91 sources = serve
92 92
93 93 [acl.deny.branches]
94 94
95 95 # Everyone is denied to the frozen branch:
96 96 frozen-branch = *
97 97
98 98 # A bad user is denied on all branches:
99 99 * = bad-user
100 100
101 101 [acl.allow.branches]
102 102
103 103 # A few users are allowed on branch-a:
104 104 branch-a = user-1, user-2, user-3
105 105
106 106 # Only one user is allowed on branch-b:
107 107 branch-b = user-1
108 108
109 109 # The super user is allowed on any branch:
110 110 * = super-user
111 111
112 112 # Everyone is allowed on branch-for-tests:
113 113 branch-for-tests = *
114 114
115 115 [acl.deny]
116 116 # This list is checked first. If a match is found, acl.allow is not
117 117 # checked. All users are granted access if acl.deny is not present.
118 118 # Format for both lists: glob pattern = user, ..., @group, ...
119 119
120 120 # To match everyone, use an asterisk for the user:
121 121 # my/glob/pattern = *
122 122
123 123 # user6 will not have write access to any file:
124 124 ** = user6
125 125
126 126 # Group "hg-denied" will not have write access to any file:
127 127 ** = @hg-denied
128 128
129 129 # Nobody will be able to change "DONT-TOUCH-THIS.txt", despite
130 130 # everyone being able to change all other files. See below.
131 131 src/main/resources/DONT-TOUCH-THIS.txt = *
132 132
133 133 [acl.allow]
134 134 # if acl.allow is not present, all users are allowed by default
135 135 # empty acl.allow = no users allowed
136 136
137 137 # User "doc_writer" has write access to any file under the "docs"
138 138 # folder:
139 139 docs/** = doc_writer
140 140
141 141 # User "jack" and group "designers" have write access to any file
142 142 # under the "images" folder:
143 143 images/** = jack, @designers
144 144
145 145 # Everyone (except for "user6" and "@hg-denied" - see acl.deny above)
146 146 # will have write access to any file under the "resources" folder
147 147 # (except for 1 file. See acl.deny):
148 148 src/main/resources/** = *
149 149
150 150 .hgtags = release_engineer
151 151
152 152 Examples using the "!" prefix
153 153 .............................
154 154
155 155 Suppose there's a branch that only a given user (or group) should be able to
156 156 push to, and you don't want to restrict access to any other branch that may
157 157 be created.
158 158
159 159 The "!" prefix allows you to prevent anyone except a given user or group to
160 160 push changesets in a given branch or path.
161 161
162 162 In the examples below, we will:
163 163 1) Deny access to branch "ring" to anyone but user "gollum"
164 164 2) Deny access to branch "lake" to anyone but members of the group "hobbit"
165 165 3) Deny access to a file to anyone but user "gollum"
166 166
167 167 ::
168 168
169 169 [acl.allow.branches]
170 170 # Empty
171 171
172 172 [acl.deny.branches]
173 173
174 174 # 1) only 'gollum' can commit to branch 'ring';
175 175 # 'gollum' and anyone else can still commit to any other branch.
176 176 ring = !gollum
177 177
178 178 # 2) only members of the group 'hobbit' can commit to branch 'lake';
179 179 # 'hobbit' members and anyone else can still commit to any other branch.
180 180 lake = !@hobbit
181 181
182 182 # You can also deny access based on file paths:
183 183
184 184 [acl.allow]
185 185 # Empty
186 186
187 187 [acl.deny]
188 188 # 3) only 'gollum' can change the file below;
189 189 # 'gollum' and anyone else can still change any other file.
190 190 /misty/mountains/cave/ring = !gollum
191 191
192 192 '''
193 193
194 194 from mercurial.i18n import _
195 195 from mercurial import util, match
196 196 import getpass, urllib
197 197
198 # Note for extension authors: ONLY specify testedwith = 'internal' for
199 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
200 # be specifying the version(s) of Mercurial they are tested with, or
201 # leave the attribute unspecified.
198 202 testedwith = 'internal'
199 203
200 204 def _getusers(ui, group):
201 205
202 206 # First, try to use group definition from section [acl.groups]
203 207 hgrcusers = ui.configlist('acl.groups', group)
204 208 if hgrcusers:
205 209 return hgrcusers
206 210
207 211 ui.debug('acl: "%s" not defined in [acl.groups]\n' % group)
208 212 # If no users found in group definition, get users from OS-level group
209 213 try:
210 214 return util.groupmembers(group)
211 215 except KeyError:
212 216 raise util.Abort(_("group '%s' is undefined") % group)
213 217
214 218 def _usermatch(ui, user, usersorgroups):
215 219
216 220 if usersorgroups == '*':
217 221 return True
218 222
219 223 for ug in usersorgroups.replace(',', ' ').split():
220 224
221 225 if ug.startswith('!'):
222 226 # Test for excluded user or group. Format:
223 227 # if ug is a user name: !username
224 228 # if ug is a group name: !@groupname
225 229 ug = ug[1:]
226 230 if not ug.startswith('@') and user != ug \
227 231 or ug.startswith('@') and user not in _getusers(ui, ug[1:]):
228 232 return True
229 233
230 234 # Test for user or group. Format:
231 235 # if ug is a user name: username
232 236 # if ug is a group name: @groupname
233 237 elif user == ug \
234 238 or ug.startswith('@') and user in _getusers(ui, ug[1:]):
235 239 return True
236 240
237 241 return False
238 242
239 243 def buildmatch(ui, repo, user, key):
240 244 '''return tuple of (match function, list enabled).'''
241 245 if not ui.has_section(key):
242 246 ui.debug('acl: %s not enabled\n' % key)
243 247 return None
244 248
245 249 pats = [pat for pat, users in ui.configitems(key)
246 250 if _usermatch(ui, user, users)]
247 251 ui.debug('acl: %s enabled, %d entries for user %s\n' %
248 252 (key, len(pats), user))
249 253
250 254 # Branch-based ACL
251 255 if not repo:
252 256 if pats:
253 257 # If there's an asterisk (meaning "any branch"), always return True;
254 258 # Otherwise, test if b is in pats
255 259 if '*' in pats:
256 260 return util.always
257 261 return lambda b: b in pats
258 262 return util.never
259 263
260 264 # Path-based ACL
261 265 if pats:
262 266 return match.match(repo.root, '', pats)
263 267 return util.never
264 268
265 269 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
266 270 if hooktype not in ['pretxnchangegroup', 'pretxncommit']:
267 271 raise util.Abort(_('config error - hook type "%s" cannot stop '
268 272 'incoming changesets nor commits') % hooktype)
269 273 if (hooktype == 'pretxnchangegroup' and
270 274 source not in ui.config('acl', 'sources', 'serve').split()):
271 275 ui.debug('acl: changes have source "%s" - skipping\n' % source)
272 276 return
273 277
274 278 user = None
275 279 if source == 'serve' and 'url' in kwargs:
276 280 url = kwargs['url'].split(':')
277 281 if url[0] == 'remote' and url[1].startswith('http'):
278 282 user = urllib.unquote(url[3])
279 283
280 284 if user is None:
281 285 user = getpass.getuser()
282 286
283 287 ui.debug('acl: checking access for user "%s"\n' % user)
284 288
285 289 cfg = ui.config('acl', 'config')
286 290 if cfg:
287 291 ui.readconfig(cfg, sections=['acl.groups', 'acl.allow.branches',
288 292 'acl.deny.branches', 'acl.allow', 'acl.deny'])
289 293
290 294 allowbranches = buildmatch(ui, None, user, 'acl.allow.branches')
291 295 denybranches = buildmatch(ui, None, user, 'acl.deny.branches')
292 296 allow = buildmatch(ui, repo, user, 'acl.allow')
293 297 deny = buildmatch(ui, repo, user, 'acl.deny')
294 298
295 299 for rev in xrange(repo[node], len(repo)):
296 300 ctx = repo[rev]
297 301 branch = ctx.branch()
298 302 if denybranches and denybranches(branch):
299 303 raise util.Abort(_('acl: user "%s" denied on branch "%s"'
300 304 ' (changeset "%s")')
301 305 % (user, branch, ctx))
302 306 if allowbranches and not allowbranches(branch):
303 307 raise util.Abort(_('acl: user "%s" not allowed on branch "%s"'
304 308 ' (changeset "%s")')
305 309 % (user, branch, ctx))
306 310 ui.debug('acl: branch access granted: "%s" on branch "%s"\n'
307 311 % (ctx, branch))
308 312
309 313 for f in ctx.files():
310 314 if deny and deny(f):
311 315 raise util.Abort(_('acl: user "%s" denied on "%s"'
312 316 ' (changeset "%s")') % (user, f, ctx))
313 317 if allow and not allow(f):
314 318 raise util.Abort(_('acl: user "%s" not allowed on "%s"'
315 319 ' (changeset "%s")') % (user, f, ctx))
316 320 ui.debug('acl: path access granted: "%s"\n' % ctx)
@@ -1,158 +1,162 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 Examples::
14 14
15 15 [blackbox]
16 16 track = *
17 17
18 18 [blackbox]
19 19 track = command, commandfinish, commandexception, exthook, pythonhook
20 20
21 21 [blackbox]
22 22 track = incoming
23 23
24 24 [blackbox]
25 25 # limit the size of a log file
26 26 maxsize = 1.5 MB
27 27 # rotate up to N log files when the current one gets too big
28 28 maxfiles = 3
29 29
30 30 """
31 31
32 32 from mercurial import util, cmdutil
33 33 from mercurial.i18n import _
34 34 import errno, os, re
35 35
36 36 cmdtable = {}
37 37 command = cmdutil.command(cmdtable)
38 # Note for extension authors: ONLY specify testedwith = 'internal' for
39 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
40 # be specifying the version(s) of Mercurial they are tested with, or
41 # leave the attribute unspecified.
38 42 testedwith = 'internal'
39 43 lastblackbox = None
40 44
41 45 def wrapui(ui):
42 46 class blackboxui(ui.__class__):
43 47 @util.propertycache
44 48 def track(self):
45 49 return self.configlist('blackbox', 'track', ['*'])
46 50
47 51 def _openlogfile(self):
48 52 def rotate(oldpath, newpath):
49 53 try:
50 54 os.unlink(newpath)
51 55 except OSError, err:
52 56 if err.errno != errno.ENOENT:
53 57 self.debug("warning: cannot remove '%s': %s\n" %
54 58 (newpath, err.strerror))
55 59 try:
56 60 if newpath:
57 61 os.rename(oldpath, newpath)
58 62 except OSError, err:
59 63 if err.errno != errno.ENOENT:
60 64 self.debug("warning: cannot rename '%s' to '%s': %s\n" %
61 65 (newpath, oldpath, err.strerror))
62 66
63 67 fp = self._bbopener('blackbox.log', 'a')
64 68 maxsize = self.configbytes('blackbox', 'maxsize', 1048576)
65 69 if maxsize > 0:
66 70 st = os.fstat(fp.fileno())
67 71 if st.st_size >= maxsize:
68 72 path = fp.name
69 73 fp.close()
70 74 maxfiles = self.configint('blackbox', 'maxfiles', 7)
71 75 for i in xrange(maxfiles - 1, 1, -1):
72 76 rotate(oldpath='%s.%d' % (path, i - 1),
73 77 newpath='%s.%d' % (path, i))
74 78 rotate(oldpath=path,
75 79 newpath=maxfiles > 0 and path + '.1')
76 80 fp = self._bbopener('blackbox.log', 'a')
77 81 return fp
78 82
79 83 def log(self, event, *msg, **opts):
80 84 global lastblackbox
81 85 super(blackboxui, self).log(event, *msg, **opts)
82 86
83 87 if not '*' in self.track and not event in self.track:
84 88 return
85 89
86 90 if util.safehasattr(self, '_blackbox'):
87 91 blackbox = self._blackbox
88 92 elif util.safehasattr(self, '_bbopener'):
89 93 try:
90 94 self._blackbox = self._openlogfile()
91 95 except (IOError, OSError), err:
92 96 self.debug('warning: cannot write to blackbox.log: %s\n' %
93 97 err.strerror)
94 98 del self._bbopener
95 99 self._blackbox = None
96 100 blackbox = self._blackbox
97 101 else:
98 102 # certain ui instances exist outside the context of
99 103 # a repo, so just default to the last blackbox that
100 104 # was seen.
101 105 blackbox = lastblackbox
102 106
103 107 if blackbox:
104 108 date = util.datestr(None, '%Y/%m/%d %H:%M:%S')
105 109 user = util.getuser()
106 110 formattedmsg = msg[0] % msg[1:]
107 111 try:
108 112 blackbox.write('%s %s> %s' % (date, user, formattedmsg))
109 113 except IOError, err:
110 114 self.debug('warning: cannot write to blackbox.log: %s\n' %
111 115 err.strerror)
112 116 lastblackbox = blackbox
113 117
114 118 def setrepo(self, repo):
115 119 self._bbopener = repo.vfs
116 120
117 121 ui.__class__ = blackboxui
118 122
119 123 def uisetup(ui):
120 124 wrapui(ui)
121 125
122 126 def reposetup(ui, repo):
123 127 # During 'hg pull' a httppeer repo is created to represent the remote repo.
124 128 # It doesn't have a .hg directory to put a blackbox in, so we don't do
125 129 # the blackbox setup for it.
126 130 if not repo.local():
127 131 return
128 132
129 133 if util.safehasattr(ui, 'setrepo'):
130 134 ui.setrepo(repo)
131 135
132 136 @command('^blackbox',
133 137 [('l', 'limit', 10, _('the number of events to show')),
134 138 ],
135 139 _('hg blackbox [OPTION]...'))
136 140 def blackbox(ui, repo, *revs, **opts):
137 141 '''view the recent repository events
138 142 '''
139 143
140 144 if not os.path.exists(repo.join('blackbox.log')):
141 145 return
142 146
143 147 limit = opts.get('limit')
144 148 blackbox = repo.vfs('blackbox.log', 'r')
145 149 lines = blackbox.read().split('\n')
146 150
147 151 count = 0
148 152 output = []
149 153 for line in reversed(lines):
150 154 if count >= limit:
151 155 break
152 156
153 157 # count the commands by matching lines like: 2013/01/23 19:13:36 root>
154 158 if re.match('^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} .*> .*', line):
155 159 count += 1
156 160 output.append(line)
157 161
158 162 ui.status('\n'.join(reversed(output)))
@@ -1,910 +1,914 b''
1 1 # bugzilla.py - bugzilla integration for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 # Copyright 2011-4 Jim Hague <jim.hague@acm.org>
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 '''hooks for integrating with the Bugzilla bug tracker
10 10
11 11 This hook extension adds comments on bugs in Bugzilla when changesets
12 12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
13 13 the Mercurial template mechanism.
14 14
15 15 The bug references can optionally include an update for Bugzilla of the
16 16 hours spent working on the bug. Bugs can also be marked fixed.
17 17
18 18 Three basic modes of access to Bugzilla are provided:
19 19
20 20 1. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
21 21
22 22 2. Check data via the Bugzilla XMLRPC interface and submit bug change
23 23 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
24 24
25 25 3. Writing directly to the Bugzilla database. Only Bugzilla installations
26 26 using MySQL are supported. Requires Python MySQLdb.
27 27
28 28 Writing directly to the database is susceptible to schema changes, and
29 29 relies on a Bugzilla contrib script to send out bug change
30 30 notification emails. This script runs as the user running Mercurial,
31 31 must be run on the host with the Bugzilla install, and requires
32 32 permission to read Bugzilla configuration details and the necessary
33 33 MySQL user and password to have full access rights to the Bugzilla
34 34 database. For these reasons this access mode is now considered
35 35 deprecated, and will not be updated for new Bugzilla versions going
36 36 forward. Only adding comments is supported in this access mode.
37 37
38 38 Access via XMLRPC needs a Bugzilla username and password to be specified
39 39 in the configuration. Comments are added under that username. Since the
40 40 configuration must be readable by all Mercurial users, it is recommended
41 41 that the rights of that user are restricted in Bugzilla to the minimum
42 42 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
43 43
44 44 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
45 45 email to the Bugzilla email interface to submit comments to bugs.
46 46 The From: address in the email is set to the email address of the Mercurial
47 47 user, so the comment appears to come from the Mercurial user. In the event
48 48 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
49 49 user, the email associated with the Bugzilla username used to log into
50 50 Bugzilla is used instead as the source of the comment. Marking bugs fixed
51 51 works on all supported Bugzilla versions.
52 52
53 53 Configuration items common to all access modes:
54 54
55 55 bugzilla.version
56 56 The access type to use. Values recognized are:
57 57
58 58 :``xmlrpc``: Bugzilla XMLRPC interface.
59 59 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
60 60 :``3.0``: MySQL access, Bugzilla 3.0 and later.
61 61 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
62 62 including 3.0.
63 63 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
64 64 including 2.18.
65 65
66 66 bugzilla.regexp
67 67 Regular expression to match bug IDs for update in changeset commit message.
68 68 It must contain one "()" named group ``<ids>`` containing the bug
69 69 IDs separated by non-digit characters. It may also contain
70 70 a named group ``<hours>`` with a floating-point number giving the
71 71 hours worked on the bug. If no named groups are present, the first
72 72 "()" group is assumed to contain the bug IDs, and work time is not
73 73 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
74 74 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
75 75 variations thereof, followed by an hours number prefixed by ``h`` or
76 76 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
77 77
78 78 bugzilla.fixregexp
79 79 Regular expression to match bug IDs for marking fixed in changeset
80 80 commit message. This must contain a "()" named group ``<ids>` containing
81 81 the bug IDs separated by non-digit characters. It may also contain
82 82 a named group ``<hours>`` with a floating-point number giving the
83 83 hours worked on the bug. If no named groups are present, the first
84 84 "()" group is assumed to contain the bug IDs, and work time is not
85 85 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
86 86 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
87 87 variations thereof, followed by an hours number prefixed by ``h`` or
88 88 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
89 89
90 90 bugzilla.fixstatus
91 91 The status to set a bug to when marking fixed. Default ``RESOLVED``.
92 92
93 93 bugzilla.fixresolution
94 94 The resolution to set a bug to when marking fixed. Default ``FIXED``.
95 95
96 96 bugzilla.style
97 97 The style file to use when formatting comments.
98 98
99 99 bugzilla.template
100 100 Template to use when formatting comments. Overrides style if
101 101 specified. In addition to the usual Mercurial keywords, the
102 102 extension specifies:
103 103
104 104 :``{bug}``: The Bugzilla bug ID.
105 105 :``{root}``: The full pathname of the Mercurial repository.
106 106 :``{webroot}``: Stripped pathname of the Mercurial repository.
107 107 :``{hgweb}``: Base URL for browsing Mercurial repositories.
108 108
109 109 Default ``changeset {node|short} in repo {root} refers to bug
110 110 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
111 111
112 112 bugzilla.strip
113 113 The number of path separator characters to strip from the front of
114 114 the Mercurial repository path (``{root}`` in templates) to produce
115 115 ``{webroot}``. For example, a repository with ``{root}``
116 116 ``/var/local/my-project`` with a strip of 2 gives a value for
117 117 ``{webroot}`` of ``my-project``. Default 0.
118 118
119 119 web.baseurl
120 120 Base URL for browsing Mercurial repositories. Referenced from
121 121 templates as ``{hgweb}``.
122 122
123 123 Configuration items common to XMLRPC+email and MySQL access modes:
124 124
125 125 bugzilla.usermap
126 126 Path of file containing Mercurial committer email to Bugzilla user email
127 127 mappings. If specified, the file should contain one mapping per
128 128 line::
129 129
130 130 committer = Bugzilla user
131 131
132 132 See also the ``[usermap]`` section.
133 133
134 134 The ``[usermap]`` section is used to specify mappings of Mercurial
135 135 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
136 136 Contains entries of the form ``committer = Bugzilla user``.
137 137
138 138 XMLRPC access mode configuration:
139 139
140 140 bugzilla.bzurl
141 141 The base URL for the Bugzilla installation.
142 142 Default ``http://localhost/bugzilla``.
143 143
144 144 bugzilla.user
145 145 The username to use to log into Bugzilla via XMLRPC. Default
146 146 ``bugs``.
147 147
148 148 bugzilla.password
149 149 The password for Bugzilla login.
150 150
151 151 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
152 152 and also:
153 153
154 154 bugzilla.bzemail
155 155 The Bugzilla email address.
156 156
157 157 In addition, the Mercurial email settings must be configured. See the
158 158 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
159 159
160 160 MySQL access mode configuration:
161 161
162 162 bugzilla.host
163 163 Hostname of the MySQL server holding the Bugzilla database.
164 164 Default ``localhost``.
165 165
166 166 bugzilla.db
167 167 Name of the Bugzilla database in MySQL. Default ``bugs``.
168 168
169 169 bugzilla.user
170 170 Username to use to access MySQL server. Default ``bugs``.
171 171
172 172 bugzilla.password
173 173 Password to use to access MySQL server.
174 174
175 175 bugzilla.timeout
176 176 Database connection timeout (seconds). Default 5.
177 177
178 178 bugzilla.bzuser
179 179 Fallback Bugzilla user name to record comments with, if changeset
180 180 committer cannot be found as a Bugzilla user.
181 181
182 182 bugzilla.bzdir
183 183 Bugzilla install directory. Used by default notify. Default
184 184 ``/var/www/html/bugzilla``.
185 185
186 186 bugzilla.notify
187 187 The command to run to get Bugzilla to send bug change notification
188 188 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
189 189 id) and ``user`` (committer bugzilla email). Default depends on
190 190 version; from 2.18 it is "cd %(bzdir)s && perl -T
191 191 contrib/sendbugmail.pl %(id)s %(user)s".
192 192
193 193 Activating the extension::
194 194
195 195 [extensions]
196 196 bugzilla =
197 197
198 198 [hooks]
199 199 # run bugzilla hook on every change pulled or pushed in here
200 200 incoming.bugzilla = python:hgext.bugzilla.hook
201 201
202 202 Example configurations:
203 203
204 204 XMLRPC example configuration. This uses the Bugzilla at
205 205 ``http://my-project.org/bugzilla``, logging in as user
206 206 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
207 207 collection of Mercurial repositories in ``/var/local/hg/repos/``,
208 208 with a web interface at ``http://my-project.org/hg``. ::
209 209
210 210 [bugzilla]
211 211 bzurl=http://my-project.org/bugzilla
212 212 user=bugmail@my-project.org
213 213 password=plugh
214 214 version=xmlrpc
215 215 template=Changeset {node|short} in {root|basename}.
216 216 {hgweb}/{webroot}/rev/{node|short}\\n
217 217 {desc}\\n
218 218 strip=5
219 219
220 220 [web]
221 221 baseurl=http://my-project.org/hg
222 222
223 223 XMLRPC+email example configuration. This uses the Bugzilla at
224 224 ``http://my-project.org/bugzilla``, logging in as user
225 225 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
226 226 collection of Mercurial repositories in ``/var/local/hg/repos/``,
227 227 with a web interface at ``http://my-project.org/hg``. Bug comments
228 228 are sent to the Bugzilla email address
229 229 ``bugzilla@my-project.org``. ::
230 230
231 231 [bugzilla]
232 232 bzurl=http://my-project.org/bugzilla
233 233 user=bugmail@my-project.org
234 234 password=plugh
235 235 version=xmlrpc+email
236 236 bzemail=bugzilla@my-project.org
237 237 template=Changeset {node|short} in {root|basename}.
238 238 {hgweb}/{webroot}/rev/{node|short}\\n
239 239 {desc}\\n
240 240 strip=5
241 241
242 242 [web]
243 243 baseurl=http://my-project.org/hg
244 244
245 245 [usermap]
246 246 user@emaildomain.com=user.name@bugzilladomain.com
247 247
248 248 MySQL example configuration. This has a local Bugzilla 3.2 installation
249 249 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
250 250 the Bugzilla database name is ``bugs`` and MySQL is
251 251 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
252 252 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
253 253 with a web interface at ``http://my-project.org/hg``. ::
254 254
255 255 [bugzilla]
256 256 host=localhost
257 257 password=XYZZY
258 258 version=3.0
259 259 bzuser=unknown@domain.com
260 260 bzdir=/opt/bugzilla-3.2
261 261 template=Changeset {node|short} in {root|basename}.
262 262 {hgweb}/{webroot}/rev/{node|short}\\n
263 263 {desc}\\n
264 264 strip=5
265 265
266 266 [web]
267 267 baseurl=http://my-project.org/hg
268 268
269 269 [usermap]
270 270 user@emaildomain.com=user.name@bugzilladomain.com
271 271
272 272 All the above add a comment to the Bugzilla bug record of the form::
273 273
274 274 Changeset 3b16791d6642 in repository-name.
275 275 http://my-project.org/hg/repository-name/rev/3b16791d6642
276 276
277 277 Changeset commit comment. Bug 1234.
278 278 '''
279 279
280 280 from mercurial.i18n import _
281 281 from mercurial.node import short
282 282 from mercurial import cmdutil, mail, util
283 283 import re, time, urlparse, xmlrpclib
284 284
285 # Note for extension authors: ONLY specify testedwith = 'internal' for
286 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
287 # be specifying the version(s) of Mercurial they are tested with, or
288 # leave the attribute unspecified.
285 289 testedwith = 'internal'
286 290
287 291 class bzaccess(object):
288 292 '''Base class for access to Bugzilla.'''
289 293
290 294 def __init__(self, ui):
291 295 self.ui = ui
292 296 usermap = self.ui.config('bugzilla', 'usermap')
293 297 if usermap:
294 298 self.ui.readconfig(usermap, sections=['usermap'])
295 299
296 300 def map_committer(self, user):
297 301 '''map name of committer to Bugzilla user name.'''
298 302 for committer, bzuser in self.ui.configitems('usermap'):
299 303 if committer.lower() == user.lower():
300 304 return bzuser
301 305 return user
302 306
303 307 # Methods to be implemented by access classes.
304 308 #
305 309 # 'bugs' is a dict keyed on bug id, where values are a dict holding
306 310 # updates to bug state. Recognized dict keys are:
307 311 #
308 312 # 'hours': Value, float containing work hours to be updated.
309 313 # 'fix': If key present, bug is to be marked fixed. Value ignored.
310 314
311 315 def filter_real_bug_ids(self, bugs):
312 316 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
313 317 pass
314 318
315 319 def filter_cset_known_bug_ids(self, node, bugs):
316 320 '''remove bug IDs where node occurs in comment text from bugs.'''
317 321 pass
318 322
319 323 def updatebug(self, bugid, newstate, text, committer):
320 324 '''update the specified bug. Add comment text and set new states.
321 325
322 326 If possible add the comment as being from the committer of
323 327 the changeset. Otherwise use the default Bugzilla user.
324 328 '''
325 329 pass
326 330
327 331 def notify(self, bugs, committer):
328 332 '''Force sending of Bugzilla notification emails.
329 333
330 334 Only required if the access method does not trigger notification
331 335 emails automatically.
332 336 '''
333 337 pass
334 338
335 339 # Bugzilla via direct access to MySQL database.
336 340 class bzmysql(bzaccess):
337 341 '''Support for direct MySQL access to Bugzilla.
338 342
339 343 The earliest Bugzilla version this is tested with is version 2.16.
340 344
341 345 If your Bugzilla is version 3.4 or above, you are strongly
342 346 recommended to use the XMLRPC access method instead.
343 347 '''
344 348
345 349 @staticmethod
346 350 def sql_buglist(ids):
347 351 '''return SQL-friendly list of bug ids'''
348 352 return '(' + ','.join(map(str, ids)) + ')'
349 353
350 354 _MySQLdb = None
351 355
352 356 def __init__(self, ui):
353 357 try:
354 358 import MySQLdb as mysql
355 359 bzmysql._MySQLdb = mysql
356 360 except ImportError, err:
357 361 raise util.Abort(_('python mysql support not available: %s') % err)
358 362
359 363 bzaccess.__init__(self, ui)
360 364
361 365 host = self.ui.config('bugzilla', 'host', 'localhost')
362 366 user = self.ui.config('bugzilla', 'user', 'bugs')
363 367 passwd = self.ui.config('bugzilla', 'password')
364 368 db = self.ui.config('bugzilla', 'db', 'bugs')
365 369 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
366 370 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
367 371 (host, db, user, '*' * len(passwd)))
368 372 self.conn = bzmysql._MySQLdb.connect(host=host,
369 373 user=user, passwd=passwd,
370 374 db=db,
371 375 connect_timeout=timeout)
372 376 self.cursor = self.conn.cursor()
373 377 self.longdesc_id = self.get_longdesc_id()
374 378 self.user_ids = {}
375 379 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
376 380
377 381 def run(self, *args, **kwargs):
378 382 '''run a query.'''
379 383 self.ui.note(_('query: %s %s\n') % (args, kwargs))
380 384 try:
381 385 self.cursor.execute(*args, **kwargs)
382 386 except bzmysql._MySQLdb.MySQLError:
383 387 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
384 388 raise
385 389
386 390 def get_longdesc_id(self):
387 391 '''get identity of longdesc field'''
388 392 self.run('select fieldid from fielddefs where name = "longdesc"')
389 393 ids = self.cursor.fetchall()
390 394 if len(ids) != 1:
391 395 raise util.Abort(_('unknown database schema'))
392 396 return ids[0][0]
393 397
394 398 def filter_real_bug_ids(self, bugs):
395 399 '''filter not-existing bugs from set.'''
396 400 self.run('select bug_id from bugs where bug_id in %s' %
397 401 bzmysql.sql_buglist(bugs.keys()))
398 402 existing = [id for (id,) in self.cursor.fetchall()]
399 403 for id in bugs.keys():
400 404 if id not in existing:
401 405 self.ui.status(_('bug %d does not exist\n') % id)
402 406 del bugs[id]
403 407
404 408 def filter_cset_known_bug_ids(self, node, bugs):
405 409 '''filter bug ids that already refer to this changeset from set.'''
406 410 self.run('''select bug_id from longdescs where
407 411 bug_id in %s and thetext like "%%%s%%"''' %
408 412 (bzmysql.sql_buglist(bugs.keys()), short(node)))
409 413 for (id,) in self.cursor.fetchall():
410 414 self.ui.status(_('bug %d already knows about changeset %s\n') %
411 415 (id, short(node)))
412 416 del bugs[id]
413 417
414 418 def notify(self, bugs, committer):
415 419 '''tell bugzilla to send mail.'''
416 420 self.ui.status(_('telling bugzilla to send mail:\n'))
417 421 (user, userid) = self.get_bugzilla_user(committer)
418 422 for id in bugs.keys():
419 423 self.ui.status(_(' bug %s\n') % id)
420 424 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
421 425 bzdir = self.ui.config('bugzilla', 'bzdir',
422 426 '/var/www/html/bugzilla')
423 427 try:
424 428 # Backwards-compatible with old notify string, which
425 429 # took one string. This will throw with a new format
426 430 # string.
427 431 cmd = cmdfmt % id
428 432 except TypeError:
429 433 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
430 434 self.ui.note(_('running notify command %s\n') % cmd)
431 435 fp = util.popen('(%s) 2>&1' % cmd)
432 436 out = fp.read()
433 437 ret = fp.close()
434 438 if ret:
435 439 self.ui.warn(out)
436 440 raise util.Abort(_('bugzilla notify command %s') %
437 441 util.explainexit(ret)[0])
438 442 self.ui.status(_('done\n'))
439 443
440 444 def get_user_id(self, user):
441 445 '''look up numeric bugzilla user id.'''
442 446 try:
443 447 return self.user_ids[user]
444 448 except KeyError:
445 449 try:
446 450 userid = int(user)
447 451 except ValueError:
448 452 self.ui.note(_('looking up user %s\n') % user)
449 453 self.run('''select userid from profiles
450 454 where login_name like %s''', user)
451 455 all = self.cursor.fetchall()
452 456 if len(all) != 1:
453 457 raise KeyError(user)
454 458 userid = int(all[0][0])
455 459 self.user_ids[user] = userid
456 460 return userid
457 461
458 462 def get_bugzilla_user(self, committer):
459 463 '''See if committer is a registered bugzilla user. Return
460 464 bugzilla username and userid if so. If not, return default
461 465 bugzilla username and userid.'''
462 466 user = self.map_committer(committer)
463 467 try:
464 468 userid = self.get_user_id(user)
465 469 except KeyError:
466 470 try:
467 471 defaultuser = self.ui.config('bugzilla', 'bzuser')
468 472 if not defaultuser:
469 473 raise util.Abort(_('cannot find bugzilla user id for %s') %
470 474 user)
471 475 userid = self.get_user_id(defaultuser)
472 476 user = defaultuser
473 477 except KeyError:
474 478 raise util.Abort(_('cannot find bugzilla user id for %s or %s')
475 479 % (user, defaultuser))
476 480 return (user, userid)
477 481
478 482 def updatebug(self, bugid, newstate, text, committer):
479 483 '''update bug state with comment text.
480 484
481 485 Try adding comment as committer of changeset, otherwise as
482 486 default bugzilla user.'''
483 487 if len(newstate) > 0:
484 488 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
485 489
486 490 (user, userid) = self.get_bugzilla_user(committer)
487 491 now = time.strftime('%Y-%m-%d %H:%M:%S')
488 492 self.run('''insert into longdescs
489 493 (bug_id, who, bug_when, thetext)
490 494 values (%s, %s, %s, %s)''',
491 495 (bugid, userid, now, text))
492 496 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
493 497 values (%s, %s, %s, %s)''',
494 498 (bugid, userid, now, self.longdesc_id))
495 499 self.conn.commit()
496 500
497 501 class bzmysql_2_18(bzmysql):
498 502 '''support for bugzilla 2.18 series.'''
499 503
500 504 def __init__(self, ui):
501 505 bzmysql.__init__(self, ui)
502 506 self.default_notify = \
503 507 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
504 508
505 509 class bzmysql_3_0(bzmysql_2_18):
506 510 '''support for bugzilla 3.0 series.'''
507 511
508 512 def __init__(self, ui):
509 513 bzmysql_2_18.__init__(self, ui)
510 514
511 515 def get_longdesc_id(self):
512 516 '''get identity of longdesc field'''
513 517 self.run('select id from fielddefs where name = "longdesc"')
514 518 ids = self.cursor.fetchall()
515 519 if len(ids) != 1:
516 520 raise util.Abort(_('unknown database schema'))
517 521 return ids[0][0]
518 522
519 523 # Bugzilla via XMLRPC interface.
520 524
521 525 class cookietransportrequest(object):
522 526 """A Transport request method that retains cookies over its lifetime.
523 527
524 528 The regular xmlrpclib transports ignore cookies. Which causes
525 529 a bit of a problem when you need a cookie-based login, as with
526 530 the Bugzilla XMLRPC interface prior to 4.4.3.
527 531
528 532 So this is a helper for defining a Transport which looks for
529 533 cookies being set in responses and saves them to add to all future
530 534 requests.
531 535 """
532 536
533 537 # Inspiration drawn from
534 538 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
535 539 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
536 540
537 541 cookies = []
538 542 def send_cookies(self, connection):
539 543 if self.cookies:
540 544 for cookie in self.cookies:
541 545 connection.putheader("Cookie", cookie)
542 546
543 547 def request(self, host, handler, request_body, verbose=0):
544 548 self.verbose = verbose
545 549 self.accept_gzip_encoding = False
546 550
547 551 # issue XML-RPC request
548 552 h = self.make_connection(host)
549 553 if verbose:
550 554 h.set_debuglevel(1)
551 555
552 556 self.send_request(h, handler, request_body)
553 557 self.send_host(h, host)
554 558 self.send_cookies(h)
555 559 self.send_user_agent(h)
556 560 self.send_content(h, request_body)
557 561
558 562 # Deal with differences between Python 2.4-2.6 and 2.7.
559 563 # In the former h is a HTTP(S). In the latter it's a
560 564 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
561 565 # HTTP(S) has an underlying HTTP(S)Connection, so extract
562 566 # that and use it.
563 567 try:
564 568 response = h.getresponse()
565 569 except AttributeError:
566 570 response = h._conn.getresponse()
567 571
568 572 # Add any cookie definitions to our list.
569 573 for header in response.msg.getallmatchingheaders("Set-Cookie"):
570 574 val = header.split(": ", 1)[1]
571 575 cookie = val.split(";", 1)[0]
572 576 self.cookies.append(cookie)
573 577
574 578 if response.status != 200:
575 579 raise xmlrpclib.ProtocolError(host + handler, response.status,
576 580 response.reason, response.msg.headers)
577 581
578 582 payload = response.read()
579 583 parser, unmarshaller = self.getparser()
580 584 parser.feed(payload)
581 585 parser.close()
582 586
583 587 return unmarshaller.close()
584 588
585 589 # The explicit calls to the underlying xmlrpclib __init__() methods are
586 590 # necessary. The xmlrpclib.Transport classes are old-style classes, and
587 591 # it turns out their __init__() doesn't get called when doing multiple
588 592 # inheritance with a new-style class.
589 593 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
590 594 def __init__(self, use_datetime=0):
591 595 if util.safehasattr(xmlrpclib.Transport, "__init__"):
592 596 xmlrpclib.Transport.__init__(self, use_datetime)
593 597
594 598 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
595 599 def __init__(self, use_datetime=0):
596 600 if util.safehasattr(xmlrpclib.Transport, "__init__"):
597 601 xmlrpclib.SafeTransport.__init__(self, use_datetime)
598 602
599 603 class bzxmlrpc(bzaccess):
600 604 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
601 605
602 606 Requires a minimum Bugzilla version 3.4.
603 607 """
604 608
605 609 def __init__(self, ui):
606 610 bzaccess.__init__(self, ui)
607 611
608 612 bzweb = self.ui.config('bugzilla', 'bzurl',
609 613 'http://localhost/bugzilla/')
610 614 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
611 615
612 616 user = self.ui.config('bugzilla', 'user', 'bugs')
613 617 passwd = self.ui.config('bugzilla', 'password')
614 618
615 619 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
616 620 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
617 621 'FIXED')
618 622
619 623 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
620 624 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
621 625 self.bzvermajor = int(ver[0])
622 626 self.bzverminor = int(ver[1])
623 627 login = self.bzproxy.User.login({'login': user, 'password': passwd,
624 628 'restrict_login': True})
625 629 self.bztoken = login.get('token', '')
626 630
627 631 def transport(self, uri):
628 632 if urlparse.urlparse(uri, "http")[0] == "https":
629 633 return cookiesafetransport()
630 634 else:
631 635 return cookietransport()
632 636
633 637 def get_bug_comments(self, id):
634 638 """Return a string with all comment text for a bug."""
635 639 c = self.bzproxy.Bug.comments({'ids': [id],
636 640 'include_fields': ['text'],
637 641 'token': self.bztoken})
638 642 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
639 643
640 644 def filter_real_bug_ids(self, bugs):
641 645 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
642 646 'include_fields': [],
643 647 'permissive': True,
644 648 'token': self.bztoken,
645 649 })
646 650 for badbug in probe['faults']:
647 651 id = badbug['id']
648 652 self.ui.status(_('bug %d does not exist\n') % id)
649 653 del bugs[id]
650 654
651 655 def filter_cset_known_bug_ids(self, node, bugs):
652 656 for id in sorted(bugs.keys()):
653 657 if self.get_bug_comments(id).find(short(node)) != -1:
654 658 self.ui.status(_('bug %d already knows about changeset %s\n') %
655 659 (id, short(node)))
656 660 del bugs[id]
657 661
658 662 def updatebug(self, bugid, newstate, text, committer):
659 663 args = {}
660 664 if 'hours' in newstate:
661 665 args['work_time'] = newstate['hours']
662 666
663 667 if self.bzvermajor >= 4:
664 668 args['ids'] = [bugid]
665 669 args['comment'] = {'body' : text}
666 670 if 'fix' in newstate:
667 671 args['status'] = self.fixstatus
668 672 args['resolution'] = self.fixresolution
669 673 args['token'] = self.bztoken
670 674 self.bzproxy.Bug.update(args)
671 675 else:
672 676 if 'fix' in newstate:
673 677 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
674 678 "to mark bugs fixed\n"))
675 679 args['id'] = bugid
676 680 args['comment'] = text
677 681 self.bzproxy.Bug.add_comment(args)
678 682
679 683 class bzxmlrpcemail(bzxmlrpc):
680 684 """Read data from Bugzilla via XMLRPC, send updates via email.
681 685
682 686 Advantages of sending updates via email:
683 687 1. Comments can be added as any user, not just logged in user.
684 688 2. Bug statuses or other fields not accessible via XMLRPC can
685 689 potentially be updated.
686 690
687 691 There is no XMLRPC function to change bug status before Bugzilla
688 692 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
689 693 But bugs can be marked fixed via email from 3.4 onwards.
690 694 """
691 695
692 696 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
693 697 # in-email fields are specified as '@<fieldname> = <value>'. In
694 698 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
695 699 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
696 700 # compatibility, but rather than rely on this use the new format for
697 701 # 4.0 onwards.
698 702
699 703 def __init__(self, ui):
700 704 bzxmlrpc.__init__(self, ui)
701 705
702 706 self.bzemail = self.ui.config('bugzilla', 'bzemail')
703 707 if not self.bzemail:
704 708 raise util.Abort(_("configuration 'bzemail' missing"))
705 709 mail.validateconfig(self.ui)
706 710
707 711 def makecommandline(self, fieldname, value):
708 712 if self.bzvermajor >= 4:
709 713 return "@%s %s" % (fieldname, str(value))
710 714 else:
711 715 if fieldname == "id":
712 716 fieldname = "bug_id"
713 717 return "@%s = %s" % (fieldname, str(value))
714 718
715 719 def send_bug_modify_email(self, bugid, commands, comment, committer):
716 720 '''send modification message to Bugzilla bug via email.
717 721
718 722 The message format is documented in the Bugzilla email_in.pl
719 723 specification. commands is a list of command lines, comment is the
720 724 comment text.
721 725
722 726 To stop users from crafting commit comments with
723 727 Bugzilla commands, specify the bug ID via the message body, rather
724 728 than the subject line, and leave a blank line after it.
725 729 '''
726 730 user = self.map_committer(committer)
727 731 matches = self.bzproxy.User.get({'match': [user],
728 732 'token': self.bztoken})
729 733 if not matches['users']:
730 734 user = self.ui.config('bugzilla', 'user', 'bugs')
731 735 matches = self.bzproxy.User.get({'match': [user],
732 736 'token': self.bztoken})
733 737 if not matches['users']:
734 738 raise util.Abort(_("default bugzilla user %s email not found") %
735 739 user)
736 740 user = matches['users'][0]['email']
737 741 commands.append(self.makecommandline("id", bugid))
738 742
739 743 text = "\n".join(commands) + "\n\n" + comment
740 744
741 745 _charsets = mail._charsets(self.ui)
742 746 user = mail.addressencode(self.ui, user, _charsets)
743 747 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
744 748 msg = mail.mimeencode(self.ui, text, _charsets)
745 749 msg['From'] = user
746 750 msg['To'] = bzemail
747 751 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
748 752 sendmail = mail.connect(self.ui)
749 753 sendmail(user, bzemail, msg.as_string())
750 754
751 755 def updatebug(self, bugid, newstate, text, committer):
752 756 cmds = []
753 757 if 'hours' in newstate:
754 758 cmds.append(self.makecommandline("work_time", newstate['hours']))
755 759 if 'fix' in newstate:
756 760 cmds.append(self.makecommandline("bug_status", self.fixstatus))
757 761 cmds.append(self.makecommandline("resolution", self.fixresolution))
758 762 self.send_bug_modify_email(bugid, cmds, text, committer)
759 763
760 764 class bugzilla(object):
761 765 # supported versions of bugzilla. different versions have
762 766 # different schemas.
763 767 _versions = {
764 768 '2.16': bzmysql,
765 769 '2.18': bzmysql_2_18,
766 770 '3.0': bzmysql_3_0,
767 771 'xmlrpc': bzxmlrpc,
768 772 'xmlrpc+email': bzxmlrpcemail
769 773 }
770 774
771 775 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
772 776 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
773 777 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
774 778
775 779 _default_fix_re = (r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
776 780 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
777 781 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
778 782 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
779 783
780 784 def __init__(self, ui, repo):
781 785 self.ui = ui
782 786 self.repo = repo
783 787
784 788 bzversion = self.ui.config('bugzilla', 'version')
785 789 try:
786 790 bzclass = bugzilla._versions[bzversion]
787 791 except KeyError:
788 792 raise util.Abort(_('bugzilla version %s not supported') %
789 793 bzversion)
790 794 self.bzdriver = bzclass(self.ui)
791 795
792 796 self.bug_re = re.compile(
793 797 self.ui.config('bugzilla', 'regexp',
794 798 bugzilla._default_bug_re), re.IGNORECASE)
795 799 self.fix_re = re.compile(
796 800 self.ui.config('bugzilla', 'fixregexp',
797 801 bugzilla._default_fix_re), re.IGNORECASE)
798 802 self.split_re = re.compile(r'\D+')
799 803
800 804 def find_bugs(self, ctx):
801 805 '''return bugs dictionary created from commit comment.
802 806
803 807 Extract bug info from changeset comments. Filter out any that are
804 808 not known to Bugzilla, and any that already have a reference to
805 809 the given changeset in their comments.
806 810 '''
807 811 start = 0
808 812 hours = 0.0
809 813 bugs = {}
810 814 bugmatch = self.bug_re.search(ctx.description(), start)
811 815 fixmatch = self.fix_re.search(ctx.description(), start)
812 816 while True:
813 817 bugattribs = {}
814 818 if not bugmatch and not fixmatch:
815 819 break
816 820 if not bugmatch:
817 821 m = fixmatch
818 822 elif not fixmatch:
819 823 m = bugmatch
820 824 else:
821 825 if bugmatch.start() < fixmatch.start():
822 826 m = bugmatch
823 827 else:
824 828 m = fixmatch
825 829 start = m.end()
826 830 if m is bugmatch:
827 831 bugmatch = self.bug_re.search(ctx.description(), start)
828 832 if 'fix' in bugattribs:
829 833 del bugattribs['fix']
830 834 else:
831 835 fixmatch = self.fix_re.search(ctx.description(), start)
832 836 bugattribs['fix'] = None
833 837
834 838 try:
835 839 ids = m.group('ids')
836 840 except IndexError:
837 841 ids = m.group(1)
838 842 try:
839 843 hours = float(m.group('hours'))
840 844 bugattribs['hours'] = hours
841 845 except IndexError:
842 846 pass
843 847 except TypeError:
844 848 pass
845 849 except ValueError:
846 850 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
847 851
848 852 for id in self.split_re.split(ids):
849 853 if not id:
850 854 continue
851 855 bugs[int(id)] = bugattribs
852 856 if bugs:
853 857 self.bzdriver.filter_real_bug_ids(bugs)
854 858 if bugs:
855 859 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
856 860 return bugs
857 861
858 862 def update(self, bugid, newstate, ctx):
859 863 '''update bugzilla bug with reference to changeset.'''
860 864
861 865 def webroot(root):
862 866 '''strip leading prefix of repo root and turn into
863 867 url-safe path.'''
864 868 count = int(self.ui.config('bugzilla', 'strip', 0))
865 869 root = util.pconvert(root)
866 870 while count > 0:
867 871 c = root.find('/')
868 872 if c == -1:
869 873 break
870 874 root = root[c + 1:]
871 875 count -= 1
872 876 return root
873 877
874 878 mapfile = self.ui.config('bugzilla', 'style')
875 879 tmpl = self.ui.config('bugzilla', 'template')
876 880 if not mapfile and not tmpl:
877 881 tmpl = _('changeset {node|short} in repo {root} refers '
878 882 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
879 883 t = cmdutil.changeset_templater(self.ui, self.repo,
880 884 False, None, tmpl, mapfile, False)
881 885 self.ui.pushbuffer()
882 886 t.show(ctx, changes=ctx.changeset(),
883 887 bug=str(bugid),
884 888 hgweb=self.ui.config('web', 'baseurl'),
885 889 root=self.repo.root,
886 890 webroot=webroot(self.repo.root))
887 891 data = self.ui.popbuffer()
888 892 self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
889 893
890 894 def notify(self, bugs, committer):
891 895 '''ensure Bugzilla users are notified of bug change.'''
892 896 self.bzdriver.notify(bugs, committer)
893 897
894 898 def hook(ui, repo, hooktype, node=None, **kwargs):
895 899 '''add comment to bugzilla for each changeset that refers to a
896 900 bugzilla bug id. only add a comment once per bug, so same change
897 901 seen multiple times does not fill bug with duplicate data.'''
898 902 if node is None:
899 903 raise util.Abort(_('hook type %s does not pass a changeset id') %
900 904 hooktype)
901 905 try:
902 906 bz = bugzilla(ui, repo)
903 907 ctx = repo[node]
904 908 bugs = bz.find_bugs(ctx)
905 909 if bugs:
906 910 for bug in bugs:
907 911 bz.update(bug, bugs[bug], ctx)
908 912 bz.notify(bugs, util.email(ctx.user()))
909 913 except Exception, e:
910 914 raise util.Abort(_('Bugzilla error: %s') % e)
@@ -1,161 +1,165 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, crytographic 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 mercurial.node import short
29 29 from mercurial import cmdutil, error, filelog, revlog, scmutil, util
30 30 from mercurial.i18n import _
31 31
32 32 cmdtable = {}
33 33 command = cmdutil.command(cmdtable)
34 # Note for extension authors: ONLY specify testedwith = 'internal' for
35 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
36 # be specifying the version(s) of Mercurial they are tested with, or
37 # leave the attribute unspecified.
34 38 testedwith = 'internal'
35 39
36 40 @command('censor',
37 41 [('r', 'rev', '', _('censor file from specified revision'), _('REV')),
38 42 ('t', 'tombstone', '', _('replacement tombstone data'), _('TEXT'))],
39 43 _('-r REV [-t TEXT] [FILE]'))
40 44 def censor(ui, repo, path, rev='', tombstone='', **opts):
41 45 if not path:
42 46 raise util.Abort(_('must specify file path to censor'))
43 47 if not rev:
44 48 raise util.Abort(_('must specify revision to censor'))
45 49
46 50 flog = repo.file(path)
47 51 if not len(flog):
48 52 raise util.Abort(_('cannot censor file with no history'))
49 53
50 54 rev = scmutil.revsingle(repo, rev, rev).rev()
51 55 try:
52 56 ctx = repo[rev]
53 57 except KeyError:
54 58 raise util.Abort(_('invalid revision identifier %s') % rev)
55 59
56 60 try:
57 61 fctx = ctx.filectx(path)
58 62 except error.LookupError:
59 63 raise util.Abort(_('file does not exist at revision %s') % rev)
60 64
61 65 fnode = fctx.filenode()
62 66 headctxs = [repo[c] for c in repo.heads()]
63 67 heads = [c for c in headctxs if path in c and c.filenode(path) == fnode]
64 68 if heads:
65 69 headlist = ', '.join([short(c.node()) for c in heads])
66 70 raise util.Abort(_('cannot censor file in heads (%s)') % headlist,
67 71 hint=_('clean/delete and commit first'))
68 72
69 73 wctx = repo[None]
70 74 wp = wctx.parents()
71 75 if ctx.node() in [p.node() for p in wp]:
72 76 raise util.Abort(_('cannot censor working directory'),
73 77 hint=_('clean/delete/update first'))
74 78
75 79 flogv = flog.version & 0xFFFF
76 80 if flogv != revlog.REVLOGNG:
77 81 raise util.Abort(
78 82 _('censor does not support revlog version %d') % (flogv,))
79 83
80 84 tombstone = filelog.packmeta({"censored": tombstone}, "")
81 85
82 86 crev = fctx.filerev()
83 87
84 88 if len(tombstone) > flog.rawsize(crev):
85 89 raise util.Abort(_(
86 90 'censor tombstone must be no longer than censored data'))
87 91
88 92 # Using two files instead of one makes it easy to rewrite entry-by-entry
89 93 idxread = repo.svfs(flog.indexfile, 'r')
90 94 idxwrite = repo.svfs(flog.indexfile, 'wb', atomictemp=True)
91 95 if flog.version & revlog.REVLOGNGINLINEDATA:
92 96 dataread, datawrite = idxread, idxwrite
93 97 else:
94 98 dataread = repo.svfs(flog.datafile, 'r')
95 99 datawrite = repo.svfs(flog.datafile, 'wb', atomictemp=True)
96 100
97 101 # Copy all revlog data up to the entry to be censored.
98 102 rio = revlog.revlogio()
99 103 offset = flog.start(crev)
100 104
101 105 for chunk in util.filechunkiter(idxread, limit=crev * rio.size):
102 106 idxwrite.write(chunk)
103 107 for chunk in util.filechunkiter(dataread, limit=offset):
104 108 datawrite.write(chunk)
105 109
106 110 def rewriteindex(r, newoffs, newdata=None):
107 111 """Rewrite the index entry with a new data offset and optional new data.
108 112
109 113 The newdata argument, if given, is a tuple of three positive integers:
110 114 (new compressed, new uncompressed, added flag bits).
111 115 """
112 116 offlags, comp, uncomp, base, link, p1, p2, nodeid = flog.index[r]
113 117 flags = revlog.gettype(offlags)
114 118 if newdata:
115 119 comp, uncomp, nflags = newdata
116 120 flags |= nflags
117 121 offlags = revlog.offset_type(newoffs, flags)
118 122 e = (offlags, comp, uncomp, r, link, p1, p2, nodeid)
119 123 idxwrite.write(rio.packentry(e, None, flog.version, r))
120 124 idxread.seek(rio.size, 1)
121 125
122 126 def rewrite(r, offs, data, nflags=revlog.REVIDX_DEFAULT_FLAGS):
123 127 """Write the given full text to the filelog with the given data offset.
124 128
125 129 Returns:
126 130 The integer number of data bytes written, for tracking data offsets.
127 131 """
128 132 flag, compdata = flog.compress(data)
129 133 newcomp = len(flag) + len(compdata)
130 134 rewriteindex(r, offs, (newcomp, len(data), nflags))
131 135 datawrite.write(flag)
132 136 datawrite.write(compdata)
133 137 dataread.seek(flog.length(r), 1)
134 138 return newcomp
135 139
136 140 # Rewrite censored revlog entry with (padded) tombstone data.
137 141 pad = ' ' * (flog.rawsize(crev) - len(tombstone))
138 142 offset += rewrite(crev, offset, tombstone + pad, revlog.REVIDX_ISCENSORED)
139 143
140 144 # Rewrite all following filelog revisions fixing up offsets and deltas.
141 145 for srev in xrange(crev + 1, len(flog)):
142 146 if crev in flog.parentrevs(srev):
143 147 # Immediate children of censored node must be re-added as fulltext.
144 148 try:
145 149 revdata = flog.revision(srev)
146 150 except error.CensoredNodeError, e:
147 151 revdata = e.tombstone
148 152 dlen = rewrite(srev, offset, revdata)
149 153 else:
150 154 # Copy any other revision data verbatim after fixing up the offset.
151 155 rewriteindex(srev, offset)
152 156 dlen = flog.length(srev)
153 157 for chunk in util.filechunkiter(dataread, limit=dlen):
154 158 datawrite.write(chunk)
155 159 offset += dlen
156 160
157 161 idxread.close()
158 162 idxwrite.close()
159 163 if dataread is not idxread:
160 164 dataread.close()
161 165 datawrite.close()
@@ -1,51 +1,55 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 mercurial import cmdutil
18 18 from mercurial.commands import templateopts
19 19 from mercurial.i18n import _
20 20
21 21 cmdtable = {}
22 22 command = cmdutil.command(cmdtable)
23 # Note for extension authors: ONLY specify testedwith = 'internal' for
24 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
25 # be specifying the version(s) of Mercurial they are tested with, or
26 # leave the attribute unspecified.
23 27 testedwith = 'internal'
24 28
25 29 @command('children',
26 30 [('r', 'rev', '',
27 31 _('show children of the specified revision'), _('REV')),
28 32 ] + templateopts,
29 33 _('hg children [-r REV] [FILE]'),
30 34 inferrepo=True)
31 35 def children(ui, repo, file_=None, **opts):
32 36 """show the children of the given or working directory revision
33 37
34 38 Print the children of the working directory's revisions. If a
35 39 revision is given via -r/--rev, the children of that revision will
36 40 be printed. If a file argument is given, revision in which the
37 41 file was last changed (after the working directory revision or the
38 42 argument to --rev if given) is printed.
39 43 """
40 44 rev = opts.get('rev')
41 45 if file_:
42 46 fctx = repo.filectx(file_, changeid=rev)
43 47 childctxs = [fcctx.changectx() for fcctx in fctx.children()]
44 48 else:
45 49 ctx = repo[rev]
46 50 childctxs = ctx.children()
47 51
48 52 displayer = cmdutil.show_changeset(ui, repo, opts)
49 53 for cctx in childctxs:
50 54 displayer.show(cctx)
51 55 displayer.close()
@@ -1,201 +1,205 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 mercurial.i18n import _
12 12 from mercurial import patch, cmdutil, scmutil, util, commands
13 13 from mercurial import encoding
14 14 import os
15 15 import time, datetime
16 16
17 17 cmdtable = {}
18 18 command = cmdutil.command(cmdtable)
19 # Note for extension authors: ONLY specify testedwith = 'internal' for
20 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
21 # be specifying the version(s) of Mercurial they are tested with, or
22 # leave the attribute unspecified.
19 23 testedwith = 'internal'
20 24
21 25 def maketemplater(ui, repo, tmpl):
22 26 try:
23 27 t = cmdutil.changeset_templater(ui, repo, False, None, tmpl,
24 28 None, False)
25 29 except SyntaxError, inst:
26 30 raise util.Abort(inst.args[0])
27 31 return t
28 32
29 33 def changedlines(ui, repo, ctx1, ctx2, fns):
30 34 added, removed = 0, 0
31 35 fmatch = scmutil.matchfiles(repo, fns)
32 36 diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
33 37 for l in diff.split('\n'):
34 38 if l.startswith("+") and not l.startswith("+++ "):
35 39 added += 1
36 40 elif l.startswith("-") and not l.startswith("--- "):
37 41 removed += 1
38 42 return (added, removed)
39 43
40 44 def countrate(ui, repo, amap, *pats, **opts):
41 45 """Calculate stats"""
42 46 if opts.get('dateformat'):
43 47 def getkey(ctx):
44 48 t, tz = ctx.date()
45 49 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
46 50 return date.strftime(opts['dateformat'])
47 51 else:
48 52 tmpl = opts.get('oldtemplate') or opts.get('template')
49 53 tmpl = maketemplater(ui, repo, tmpl)
50 54 def getkey(ctx):
51 55 ui.pushbuffer()
52 56 tmpl.show(ctx)
53 57 return ui.popbuffer()
54 58
55 59 state = {'count': 0}
56 60 rate = {}
57 61 df = False
58 62 if opts.get('date'):
59 63 df = util.matchdate(opts['date'])
60 64
61 65 m = scmutil.match(repo[None], pats, opts)
62 66 def prep(ctx, fns):
63 67 rev = ctx.rev()
64 68 if df and not df(ctx.date()[0]): # doesn't match date format
65 69 return
66 70
67 71 key = getkey(ctx).strip()
68 72 key = amap.get(key, key) # alias remap
69 73 if opts.get('changesets'):
70 74 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
71 75 else:
72 76 parents = ctx.parents()
73 77 if len(parents) > 1:
74 78 ui.note(_('revision %d is a merge, ignoring...\n') % (rev,))
75 79 return
76 80
77 81 ctx1 = parents[0]
78 82 lines = changedlines(ui, repo, ctx1, ctx, fns)
79 83 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
80 84
81 85 state['count'] += 1
82 86 ui.progress(_('analyzing'), state['count'], total=len(repo))
83 87
84 88 for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
85 89 continue
86 90
87 91 ui.progress(_('analyzing'), None)
88 92
89 93 return rate
90 94
91 95
92 96 @command('churn',
93 97 [('r', 'rev', [],
94 98 _('count rate for the specified revision or revset'), _('REV')),
95 99 ('d', 'date', '',
96 100 _('count rate for revisions matching date spec'), _('DATE')),
97 101 ('t', 'oldtemplate', '',
98 102 _('template to group changesets (DEPRECATED)'), _('TEMPLATE')),
99 103 ('T', 'template', '{author|email}',
100 104 _('template to group changesets'), _('TEMPLATE')),
101 105 ('f', 'dateformat', '',
102 106 _('strftime-compatible format for grouping by date'), _('FORMAT')),
103 107 ('c', 'changesets', False, _('count rate by number of changesets')),
104 108 ('s', 'sort', False, _('sort by key (default: sort by count)')),
105 109 ('', 'diffstat', False, _('display added/removed lines separately')),
106 110 ('', 'aliases', '', _('file with email aliases'), _('FILE')),
107 111 ] + commands.walkopts,
108 112 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"),
109 113 inferrepo=True)
110 114 def churn(ui, repo, *pats, **opts):
111 115 '''histogram of changes to the repository
112 116
113 117 This command will display a histogram representing the number
114 118 of changed lines or revisions, grouped according to the given
115 119 template. The default template will group changes by author.
116 120 The --dateformat option may be used to group the results by
117 121 date instead.
118 122
119 123 Statistics are based on the number of changed lines, or
120 124 alternatively the number of matching revisions if the
121 125 --changesets option is specified.
122 126
123 127 Examples::
124 128
125 129 # display count of changed lines for every committer
126 130 hg churn -t "{author|email}"
127 131
128 132 # display daily activity graph
129 133 hg churn -f "%H" -s -c
130 134
131 135 # display activity of developers by month
132 136 hg churn -f "%Y-%m" -s -c
133 137
134 138 # display count of lines changed in every year
135 139 hg churn -f "%Y" -s
136 140
137 141 It is possible to map alternate email addresses to a main address
138 142 by providing a file using the following format::
139 143
140 144 <alias email> = <actual email>
141 145
142 146 Such a file may be specified with the --aliases option, otherwise
143 147 a .hgchurn file will be looked for in the working directory root.
144 148 Aliases will be split from the rightmost "=".
145 149 '''
146 150 def pad(s, l):
147 151 return s + " " * (l - encoding.colwidth(s))
148 152
149 153 amap = {}
150 154 aliases = opts.get('aliases')
151 155 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
152 156 aliases = repo.wjoin('.hgchurn')
153 157 if aliases:
154 158 for l in open(aliases, "r"):
155 159 try:
156 160 alias, actual = l.rsplit('=' in l and '=' or None, 1)
157 161 amap[alias.strip()] = actual.strip()
158 162 except ValueError:
159 163 l = l.strip()
160 164 if l:
161 165 ui.warn(_("skipping malformed alias: %s\n") % l)
162 166 continue
163 167
164 168 rate = countrate(ui, repo, amap, *pats, **opts).items()
165 169 if not rate:
166 170 return
167 171
168 172 if opts.get('sort'):
169 173 rate.sort()
170 174 else:
171 175 rate.sort(key=lambda x: (-sum(x[1]), x))
172 176
173 177 # Be careful not to have a zero maxcount (issue833)
174 178 maxcount = float(max(sum(v) for k, v in rate)) or 1.0
175 179 maxname = max(len(k) for k, v in rate)
176 180
177 181 ttywidth = ui.termwidth()
178 182 ui.debug("assuming %i character terminal\n" % ttywidth)
179 183 width = ttywidth - maxname - 2 - 2 - 2
180 184
181 185 if opts.get('diffstat'):
182 186 width -= 15
183 187 def format(name, diffstat):
184 188 added, removed = diffstat
185 189 return "%s %15s %s%s\n" % (pad(name, maxname),
186 190 '+%d/-%d' % (added, removed),
187 191 ui.label('+' * charnum(added),
188 192 'diffstat.inserted'),
189 193 ui.label('-' * charnum(removed),
190 194 'diffstat.deleted'))
191 195 else:
192 196 width -= 6
193 197 def format(name, count):
194 198 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
195 199 '*' * charnum(sum(count)))
196 200
197 201 def charnum(count):
198 202 return int(round(count * width / maxcount))
199 203
200 204 for name, count in rate:
201 205 ui.write(format(name, count))
@@ -1,683 +1,687 b''
1 1 # color.py color output for Mercurial commands
2 2 #
3 3 # Copyright (C) 2007 Kevin Christen <kevin.christen@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 '''colorize output from some commands
9 9
10 10 The color extension colorizes output from several Mercurial commands.
11 11 For example, the diff command shows additions in green and deletions
12 12 in red, while the status command shows modified files in magenta. Many
13 13 other commands have analogous colors. It is possible to customize
14 14 these colors.
15 15
16 16 Effects
17 17 -------
18 18
19 19 Other effects in addition to color, like bold and underlined text, are
20 20 also available. By default, the terminfo database is used to find the
21 21 terminal codes used to change color and effect. If terminfo is not
22 22 available, then effects are rendered with the ECMA-48 SGR control
23 23 function (aka ANSI escape codes).
24 24
25 25 The available effects in terminfo mode are 'blink', 'bold', 'dim',
26 26 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
27 27 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
28 28 'underline'. How each is rendered depends on the terminal emulator.
29 29 Some may not be available for a given terminal type, and will be
30 30 silently ignored.
31 31
32 32 Labels
33 33 ------
34 34
35 35 Text receives color effects depending on the labels that it has. Many
36 36 default Mercurial commands emit labelled text. You can also define
37 37 your own labels in templates using the label function, see :hg:`help
38 38 templates`. A single portion of text may have more than one label. In
39 39 that case, effects given to the last label will override any other
40 40 effects. This includes the special "none" effect, which nullifies
41 41 other effects.
42 42
43 43 Labels are normally invisible. In order to see these labels and their
44 44 position in the text, use the global --color=debug option. The same
45 45 anchor text may be associated to multiple labels, e.g.
46 46
47 47 [log.changeset changeset.secret|changeset: 22611:6f0a53c8f587]
48 48
49 49 The following are the default effects for some default labels. Default
50 50 effects may be overridden from your configuration file::
51 51
52 52 [color]
53 53 status.modified = blue bold underline red_background
54 54 status.added = green bold
55 55 status.removed = red bold blue_background
56 56 status.deleted = cyan bold underline
57 57 status.unknown = magenta bold underline
58 58 status.ignored = black bold
59 59
60 60 # 'none' turns off all effects
61 61 status.clean = none
62 62 status.copied = none
63 63
64 64 qseries.applied = blue bold underline
65 65 qseries.unapplied = black bold
66 66 qseries.missing = red bold
67 67
68 68 diff.diffline = bold
69 69 diff.extended = cyan bold
70 70 diff.file_a = red bold
71 71 diff.file_b = green bold
72 72 diff.hunk = magenta
73 73 diff.deleted = red
74 74 diff.inserted = green
75 75 diff.changed = white
76 76 diff.tab =
77 77 diff.trailingwhitespace = bold red_background
78 78
79 79 # Blank so it inherits the style of the surrounding label
80 80 changeset.public =
81 81 changeset.draft =
82 82 changeset.secret =
83 83
84 84 resolve.unresolved = red bold
85 85 resolve.resolved = green bold
86 86
87 87 bookmarks.current = green
88 88
89 89 branches.active = none
90 90 branches.closed = black bold
91 91 branches.current = green
92 92 branches.inactive = none
93 93
94 94 tags.normal = green
95 95 tags.local = black bold
96 96
97 97 rebase.rebased = blue
98 98 rebase.remaining = red bold
99 99
100 100 shelve.age = cyan
101 101 shelve.newest = green bold
102 102 shelve.name = blue bold
103 103
104 104 histedit.remaining = red bold
105 105
106 106 Custom colors
107 107 -------------
108 108
109 109 Because there are only eight standard colors, this module allows you
110 110 to define color names for other color slots which might be available
111 111 for your terminal type, assuming terminfo mode. For instance::
112 112
113 113 color.brightblue = 12
114 114 color.pink = 207
115 115 color.orange = 202
116 116
117 117 to set 'brightblue' to color slot 12 (useful for 16 color terminals
118 118 that have brighter colors defined in the upper eight) and, 'pink' and
119 119 'orange' to colors in 256-color xterm's default color cube. These
120 120 defined colors may then be used as any of the pre-defined eight,
121 121 including appending '_background' to set the background to that color.
122 122
123 123 Modes
124 124 -----
125 125
126 126 By default, the color extension will use ANSI mode (or win32 mode on
127 127 Windows) if it detects a terminal. To override auto mode (to enable
128 128 terminfo mode, for example), set the following configuration option::
129 129
130 130 [color]
131 131 mode = terminfo
132 132
133 133 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
134 134 disable color.
135 135
136 136 Note that on some systems, terminfo mode may cause problems when using
137 137 color with the pager extension and less -R. less with the -R option
138 138 will only display ECMA-48 color codes, and terminfo mode may sometimes
139 139 emit codes that less doesn't understand. You can work around this by
140 140 either using ansi mode (or auto mode), or by using less -r (which will
141 141 pass through all terminal control codes, not just color control
142 142 codes).
143 143
144 144 On some systems (such as MSYS in Windows), the terminal may support
145 145 a different color mode than the pager (activated via the "pager"
146 146 extension). It is possible to define separate modes depending on whether
147 147 the pager is active::
148 148
149 149 [color]
150 150 mode = auto
151 151 pagermode = ansi
152 152
153 153 If ``pagermode`` is not defined, the ``mode`` will be used.
154 154 '''
155 155
156 156 import os
157 157
158 158 from mercurial import cmdutil, commands, dispatch, extensions, subrepo, util
159 159 from mercurial import ui as uimod
160 160 from mercurial import templater, error
161 161 from mercurial.i18n import _
162 162
163 163 cmdtable = {}
164 164 command = cmdutil.command(cmdtable)
165 # Note for extension authors: ONLY specify testedwith = 'internal' for
166 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
167 # be specifying the version(s) of Mercurial they are tested with, or
168 # leave the attribute unspecified.
165 169 testedwith = 'internal'
166 170
167 171 # start and stop parameters for effects
168 172 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
169 173 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
170 174 'italic': 3, 'underline': 4, 'inverse': 7, 'dim': 2,
171 175 'black_background': 40, 'red_background': 41,
172 176 'green_background': 42, 'yellow_background': 43,
173 177 'blue_background': 44, 'purple_background': 45,
174 178 'cyan_background': 46, 'white_background': 47}
175 179
176 180 def _terminfosetup(ui, mode):
177 181 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
178 182
179 183 global _terminfo_params
180 184 # If we failed to load curses, we go ahead and return.
181 185 if not _terminfo_params:
182 186 return
183 187 # Otherwise, see what the config file says.
184 188 if mode not in ('auto', 'terminfo'):
185 189 return
186 190
187 191 _terminfo_params.update((key[6:], (False, int(val)))
188 192 for key, val in ui.configitems('color')
189 193 if key.startswith('color.'))
190 194
191 195 try:
192 196 curses.setupterm()
193 197 except curses.error, e:
194 198 _terminfo_params = {}
195 199 return
196 200
197 201 for key, (b, e) in _terminfo_params.items():
198 202 if not b:
199 203 continue
200 204 if not curses.tigetstr(e):
201 205 # Most terminals don't support dim, invis, etc, so don't be
202 206 # noisy and use ui.debug().
203 207 ui.debug("no terminfo entry for %s\n" % e)
204 208 del _terminfo_params[key]
205 209 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
206 210 # Only warn about missing terminfo entries if we explicitly asked for
207 211 # terminfo mode.
208 212 if mode == "terminfo":
209 213 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
210 214 "ECMA-48 color\n"))
211 215 _terminfo_params = {}
212 216
213 217 def _modesetup(ui, coloropt):
214 218 global _terminfo_params
215 219
216 220 if coloropt == 'debug':
217 221 return 'debug'
218 222
219 223 auto = (coloropt == 'auto')
220 224 always = not auto and util.parsebool(coloropt)
221 225 if not always and not auto:
222 226 return None
223 227
224 228 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
225 229
226 230 mode = ui.config('color', 'mode', 'auto')
227 231
228 232 # If pager is active, color.pagermode overrides color.mode.
229 233 if getattr(ui, 'pageractive', False):
230 234 mode = ui.config('color', 'pagermode', mode)
231 235
232 236 realmode = mode
233 237 if mode == 'auto':
234 238 if os.name == 'nt':
235 239 term = os.environ.get('TERM')
236 240 # TERM won't be defined in a vanilla cmd.exe environment.
237 241
238 242 # UNIX-like environments on Windows such as Cygwin and MSYS will
239 243 # set TERM. They appear to make a best effort attempt at setting it
240 244 # to something appropriate. However, not all environments with TERM
241 245 # defined support ANSI. Since "ansi" could result in terminal
242 246 # gibberish, we error on the side of selecting "win32". However, if
243 247 # w32effects is not defined, we almost certainly don't support
244 248 # "win32", so don't even try.
245 249 if (term and 'xterm' in term) or not w32effects:
246 250 realmode = 'ansi'
247 251 else:
248 252 realmode = 'win32'
249 253 else:
250 254 realmode = 'ansi'
251 255
252 256 def modewarn():
253 257 # only warn if color.mode was explicitly set and we're in
254 258 # an interactive terminal
255 259 if mode == realmode and ui.interactive():
256 260 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
257 261
258 262 if realmode == 'win32':
259 263 _terminfo_params = {}
260 264 if not w32effects:
261 265 modewarn()
262 266 return None
263 267 _effects.update(w32effects)
264 268 elif realmode == 'ansi':
265 269 _terminfo_params = {}
266 270 elif realmode == 'terminfo':
267 271 _terminfosetup(ui, mode)
268 272 if not _terminfo_params:
269 273 ## FIXME Shouldn't we return None in this case too?
270 274 modewarn()
271 275 realmode = 'ansi'
272 276 else:
273 277 return None
274 278
275 279 if always or (auto and formatted):
276 280 return realmode
277 281 return None
278 282
279 283 try:
280 284 import curses
281 285 # Mapping from effect name to terminfo attribute name or color number.
282 286 # This will also force-load the curses module.
283 287 _terminfo_params = {'none': (True, 'sgr0'),
284 288 'standout': (True, 'smso'),
285 289 'underline': (True, 'smul'),
286 290 'reverse': (True, 'rev'),
287 291 'inverse': (True, 'rev'),
288 292 'blink': (True, 'blink'),
289 293 'dim': (True, 'dim'),
290 294 'bold': (True, 'bold'),
291 295 'invisible': (True, 'invis'),
292 296 'italic': (True, 'sitm'),
293 297 'black': (False, curses.COLOR_BLACK),
294 298 'red': (False, curses.COLOR_RED),
295 299 'green': (False, curses.COLOR_GREEN),
296 300 'yellow': (False, curses.COLOR_YELLOW),
297 301 'blue': (False, curses.COLOR_BLUE),
298 302 'magenta': (False, curses.COLOR_MAGENTA),
299 303 'cyan': (False, curses.COLOR_CYAN),
300 304 'white': (False, curses.COLOR_WHITE)}
301 305 except ImportError:
302 306 _terminfo_params = {}
303 307
304 308 _styles = {'grep.match': 'red bold',
305 309 'grep.linenumber': 'green',
306 310 'grep.rev': 'green',
307 311 'grep.change': 'green',
308 312 'grep.sep': 'cyan',
309 313 'grep.filename': 'magenta',
310 314 'grep.user': 'magenta',
311 315 'grep.date': 'magenta',
312 316 'bookmarks.current': 'green',
313 317 'branches.active': 'none',
314 318 'branches.closed': 'black bold',
315 319 'branches.current': 'green',
316 320 'branches.inactive': 'none',
317 321 'diff.changed': 'white',
318 322 'diff.deleted': 'red',
319 323 'diff.diffline': 'bold',
320 324 'diff.extended': 'cyan bold',
321 325 'diff.file_a': 'red bold',
322 326 'diff.file_b': 'green bold',
323 327 'diff.hunk': 'magenta',
324 328 'diff.inserted': 'green',
325 329 'diff.tab': '',
326 330 'diff.trailingwhitespace': 'bold red_background',
327 331 'changeset.public' : '',
328 332 'changeset.draft' : '',
329 333 'changeset.secret' : '',
330 334 'diffstat.deleted': 'red',
331 335 'diffstat.inserted': 'green',
332 336 'histedit.remaining': 'red bold',
333 337 'ui.prompt': 'yellow',
334 338 'log.changeset': 'yellow',
335 339 'patchbomb.finalsummary': '',
336 340 'patchbomb.from': 'magenta',
337 341 'patchbomb.to': 'cyan',
338 342 'patchbomb.subject': 'green',
339 343 'patchbomb.diffstats': '',
340 344 'rebase.rebased': 'blue',
341 345 'rebase.remaining': 'red bold',
342 346 'resolve.resolved': 'green bold',
343 347 'resolve.unresolved': 'red bold',
344 348 'shelve.age': 'cyan',
345 349 'shelve.newest': 'green bold',
346 350 'shelve.name': 'blue bold',
347 351 'status.added': 'green bold',
348 352 'status.clean': 'none',
349 353 'status.copied': 'none',
350 354 'status.deleted': 'cyan bold underline',
351 355 'status.ignored': 'black bold',
352 356 'status.modified': 'blue bold',
353 357 'status.removed': 'red bold',
354 358 'status.unknown': 'magenta bold underline',
355 359 'tags.normal': 'green',
356 360 'tags.local': 'black bold'}
357 361
358 362
359 363 def _effect_str(effect):
360 364 '''Helper function for render_effects().'''
361 365
362 366 bg = False
363 367 if effect.endswith('_background'):
364 368 bg = True
365 369 effect = effect[:-11]
366 370 attr, val = _terminfo_params[effect]
367 371 if attr:
368 372 return curses.tigetstr(val)
369 373 elif bg:
370 374 return curses.tparm(curses.tigetstr('setab'), val)
371 375 else:
372 376 return curses.tparm(curses.tigetstr('setaf'), val)
373 377
374 378 def render_effects(text, effects):
375 379 'Wrap text in commands to turn on each effect.'
376 380 if not text:
377 381 return text
378 382 if not _terminfo_params:
379 383 start = [str(_effects[e]) for e in ['none'] + effects.split()]
380 384 start = '\033[' + ';'.join(start) + 'm'
381 385 stop = '\033[' + str(_effects['none']) + 'm'
382 386 else:
383 387 start = ''.join(_effect_str(effect)
384 388 for effect in ['none'] + effects.split())
385 389 stop = _effect_str('none')
386 390 return ''.join([start, text, stop])
387 391
388 392 def extstyles():
389 393 for name, ext in extensions.extensions():
390 394 _styles.update(getattr(ext, 'colortable', {}))
391 395
392 396 def valideffect(effect):
393 397 'Determine if the effect is valid or not.'
394 398 good = False
395 399 if not _terminfo_params and effect in _effects:
396 400 good = True
397 401 elif effect in _terminfo_params or effect[:-11] in _terminfo_params:
398 402 good = True
399 403 return good
400 404
401 405 def configstyles(ui):
402 406 for status, cfgeffects in ui.configitems('color'):
403 407 if '.' not in status or status.startswith('color.'):
404 408 continue
405 409 cfgeffects = ui.configlist('color', status)
406 410 if cfgeffects:
407 411 good = []
408 412 for e in cfgeffects:
409 413 if valideffect(e):
410 414 good.append(e)
411 415 else:
412 416 ui.warn(_("ignoring unknown color/effect %r "
413 417 "(configured in color.%s)\n")
414 418 % (e, status))
415 419 _styles[status] = ' '.join(good)
416 420
417 421 class colorui(uimod.ui):
418 422 def popbuffer(self, labeled=False):
419 423 if self._colormode is None:
420 424 return super(colorui, self).popbuffer(labeled)
421 425
422 426 self._bufferstates.pop()
423 427 if labeled:
424 428 return ''.join(self.label(a, label) for a, label
425 429 in self._buffers.pop())
426 430 return ''.join(a for a, label in self._buffers.pop())
427 431
428 432 _colormode = 'ansi'
429 433 def write(self, *args, **opts):
430 434 if self._colormode is None:
431 435 return super(colorui, self).write(*args, **opts)
432 436
433 437 label = opts.get('label', '')
434 438 if self._buffers:
435 439 self._buffers[-1].extend([(str(a), label) for a in args])
436 440 elif self._colormode == 'win32':
437 441 for a in args:
438 442 win32print(a, super(colorui, self).write, **opts)
439 443 else:
440 444 return super(colorui, self).write(
441 445 *[self.label(str(a), label) for a in args], **opts)
442 446
443 447 def write_err(self, *args, **opts):
444 448 if self._colormode is None:
445 449 return super(colorui, self).write_err(*args, **opts)
446 450
447 451 label = opts.get('label', '')
448 452 if self._bufferstates and self._bufferstates[-1][0]:
449 453 return self.write(*args, **opts)
450 454 if self._colormode == 'win32':
451 455 for a in args:
452 456 win32print(a, super(colorui, self).write_err, **opts)
453 457 else:
454 458 return super(colorui, self).write_err(
455 459 *[self.label(str(a), label) for a in args], **opts)
456 460
457 461 def showlabel(self, msg, label):
458 462 if label and msg:
459 463 if msg[-1] == '\n':
460 464 return "[%s|%s]\n" % (label, msg[:-1])
461 465 else:
462 466 return "[%s|%s]" % (label, msg)
463 467 else:
464 468 return msg
465 469
466 470 def label(self, msg, label):
467 471 if self._colormode is None:
468 472 return super(colorui, self).label(msg, label)
469 473
470 474 if self._colormode == 'debug':
471 475 return self.showlabel(msg, label)
472 476
473 477 effects = []
474 478 for l in label.split():
475 479 s = _styles.get(l, '')
476 480 if s:
477 481 effects.append(s)
478 482 elif valideffect(l):
479 483 effects.append(l)
480 484 effects = ' '.join(effects)
481 485 if effects:
482 486 return '\n'.join([render_effects(s, effects)
483 487 for s in msg.split('\n')])
484 488 return msg
485 489
486 490 def templatelabel(context, mapping, args):
487 491 if len(args) != 2:
488 492 # i18n: "label" is a keyword
489 493 raise error.ParseError(_("label expects two arguments"))
490 494
491 495 # add known effects to the mapping so symbols like 'red', 'bold',
492 496 # etc. don't need to be quoted
493 497 mapping.update(dict([(k, k) for k in _effects]))
494 498
495 499 thing = templater._evalifliteral(args[1], context, mapping)
496 500
497 501 # apparently, repo could be a string that is the favicon?
498 502 repo = mapping.get('repo', '')
499 503 if isinstance(repo, str):
500 504 return thing
501 505
502 506 label = templater._evalifliteral(args[0], context, mapping)
503 507
504 508 thing = templater.stringify(thing)
505 509 label = templater.stringify(label)
506 510
507 511 return repo.ui.label(thing, label)
508 512
509 513 def uisetup(ui):
510 514 if ui.plain():
511 515 return
512 516 if not isinstance(ui, colorui):
513 517 colorui.__bases__ = (ui.__class__,)
514 518 ui.__class__ = colorui
515 519 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
516 520 mode = _modesetup(ui_, opts['color'])
517 521 colorui._colormode = mode
518 522 if mode and mode != 'debug':
519 523 extstyles()
520 524 configstyles(ui_)
521 525 return orig(ui_, opts, cmd, cmdfunc)
522 526 def colorgit(orig, gitsub, commands, env=None, stream=False, cwd=None):
523 527 if gitsub.ui._colormode and len(commands) and commands[0] == "diff":
524 528 # insert the argument in the front,
525 529 # the end of git diff arguments is used for paths
526 530 commands.insert(1, '--color')
527 531 return orig(gitsub, commands, env, stream, cwd)
528 532 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
529 533 extensions.wrapfunction(subrepo.gitsubrepo, '_gitnodir', colorgit)
530 534 templater.funcs['label'] = templatelabel
531 535
532 536 def extsetup(ui):
533 537 commands.globalopts.append(
534 538 ('', 'color', 'auto',
535 539 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
536 540 # and should not be translated
537 541 _("when to colorize (boolean, always, auto, never, or debug)"),
538 542 _('TYPE')))
539 543
540 544 @command('debugcolor', [], 'hg debugcolor')
541 545 def debugcolor(ui, repo, **opts):
542 546 global _styles
543 547 _styles = {}
544 548 for effect in _effects.keys():
545 549 _styles[effect] = effect
546 550 ui.write(('color mode: %s\n') % ui._colormode)
547 551 ui.write(_('available colors:\n'))
548 552 for label, colors in _styles.items():
549 553 ui.write(('%s\n') % colors, label=label)
550 554
551 555 if os.name != 'nt':
552 556 w32effects = None
553 557 else:
554 558 import re, ctypes
555 559
556 560 _kernel32 = ctypes.windll.kernel32
557 561
558 562 _WORD = ctypes.c_ushort
559 563
560 564 _INVALID_HANDLE_VALUE = -1
561 565
562 566 class _COORD(ctypes.Structure):
563 567 _fields_ = [('X', ctypes.c_short),
564 568 ('Y', ctypes.c_short)]
565 569
566 570 class _SMALL_RECT(ctypes.Structure):
567 571 _fields_ = [('Left', ctypes.c_short),
568 572 ('Top', ctypes.c_short),
569 573 ('Right', ctypes.c_short),
570 574 ('Bottom', ctypes.c_short)]
571 575
572 576 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
573 577 _fields_ = [('dwSize', _COORD),
574 578 ('dwCursorPosition', _COORD),
575 579 ('wAttributes', _WORD),
576 580 ('srWindow', _SMALL_RECT),
577 581 ('dwMaximumWindowSize', _COORD)]
578 582
579 583 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
580 584 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
581 585
582 586 _FOREGROUND_BLUE = 0x0001
583 587 _FOREGROUND_GREEN = 0x0002
584 588 _FOREGROUND_RED = 0x0004
585 589 _FOREGROUND_INTENSITY = 0x0008
586 590
587 591 _BACKGROUND_BLUE = 0x0010
588 592 _BACKGROUND_GREEN = 0x0020
589 593 _BACKGROUND_RED = 0x0040
590 594 _BACKGROUND_INTENSITY = 0x0080
591 595
592 596 _COMMON_LVB_REVERSE_VIDEO = 0x4000
593 597 _COMMON_LVB_UNDERSCORE = 0x8000
594 598
595 599 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
596 600 w32effects = {
597 601 'none': -1,
598 602 'black': 0,
599 603 'red': _FOREGROUND_RED,
600 604 'green': _FOREGROUND_GREEN,
601 605 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
602 606 'blue': _FOREGROUND_BLUE,
603 607 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
604 608 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
605 609 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
606 610 'bold': _FOREGROUND_INTENSITY,
607 611 'black_background': 0x100, # unused value > 0x0f
608 612 'red_background': _BACKGROUND_RED,
609 613 'green_background': _BACKGROUND_GREEN,
610 614 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
611 615 'blue_background': _BACKGROUND_BLUE,
612 616 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
613 617 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
614 618 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
615 619 _BACKGROUND_BLUE),
616 620 'bold_background': _BACKGROUND_INTENSITY,
617 621 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
618 622 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
619 623 }
620 624
621 625 passthrough = set([_FOREGROUND_INTENSITY,
622 626 _BACKGROUND_INTENSITY,
623 627 _COMMON_LVB_UNDERSCORE,
624 628 _COMMON_LVB_REVERSE_VIDEO])
625 629
626 630 stdout = _kernel32.GetStdHandle(
627 631 _STD_OUTPUT_HANDLE) # don't close the handle returned
628 632 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
629 633 w32effects = None
630 634 else:
631 635 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
632 636 if not _kernel32.GetConsoleScreenBufferInfo(
633 637 stdout, ctypes.byref(csbi)):
634 638 # stdout may not support GetConsoleScreenBufferInfo()
635 639 # when called from subprocess or redirected
636 640 w32effects = None
637 641 else:
638 642 origattr = csbi.wAttributes
639 643 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
640 644 re.MULTILINE | re.DOTALL)
641 645
642 646 def win32print(text, orig, **opts):
643 647 label = opts.get('label', '')
644 648 attr = origattr
645 649
646 650 def mapcolor(val, attr):
647 651 if val == -1:
648 652 return origattr
649 653 elif val in passthrough:
650 654 return attr | val
651 655 elif val > 0x0f:
652 656 return (val & 0x70) | (attr & 0x8f)
653 657 else:
654 658 return (val & 0x07) | (attr & 0xf8)
655 659
656 660 # determine console attributes based on labels
657 661 for l in label.split():
658 662 style = _styles.get(l, '')
659 663 for effect in style.split():
660 664 try:
661 665 attr = mapcolor(w32effects[effect], attr)
662 666 except KeyError:
663 667 # w32effects could not have certain attributes so we skip
664 668 # them if not found
665 669 pass
666 670 # hack to ensure regexp finds data
667 671 if not text.startswith('\033['):
668 672 text = '\033[m' + text
669 673
670 674 # Look for ANSI-like codes embedded in text
671 675 m = re.match(ansire, text)
672 676
673 677 try:
674 678 while m:
675 679 for sattr in m.group(1).split(';'):
676 680 if sattr:
677 681 attr = mapcolor(int(sattr), attr)
678 682 _kernel32.SetConsoleTextAttribute(stdout, attr)
679 683 orig(m.group(2), **opts)
680 684 m = re.match(ansire, m.group(3))
681 685 finally:
682 686 # Explicitly reset original attributes
683 687 _kernel32.SetConsoleTextAttribute(stdout, origattr)
@@ -1,405 +1,409 b''
1 1 # convert.py Foreign SCM converter
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.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 '''import revisions from foreign VCS repositories into Mercurial'''
9 9
10 10 import convcmd
11 11 import cvsps
12 12 import subversion
13 13 from mercurial import cmdutil, templatekw
14 14 from mercurial.i18n import _
15 15
16 16 cmdtable = {}
17 17 command = cmdutil.command(cmdtable)
18 # Note for extension authors: ONLY specify testedwith = 'internal' for
19 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
20 # be specifying the version(s) of Mercurial they are tested with, or
21 # leave the attribute unspecified.
18 22 testedwith = 'internal'
19 23
20 24 # Commands definition was moved elsewhere to ease demandload job.
21 25
22 26 @command('convert',
23 27 [('', 'authors', '',
24 28 _('username mapping filename (DEPRECATED, use --authormap instead)'),
25 29 _('FILE')),
26 30 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
27 31 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
28 32 ('r', 'rev', '', _('import up to source revision REV'), _('REV')),
29 33 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
30 34 ('', 'filemap', '', _('remap file names using contents of file'),
31 35 _('FILE')),
32 36 ('', 'full', None,
33 37 _('apply filemap changes by converting all files again')),
34 38 ('', 'splicemap', '', _('splice synthesized history into place'),
35 39 _('FILE')),
36 40 ('', 'branchmap', '', _('change branch names while converting'),
37 41 _('FILE')),
38 42 ('', 'branchsort', None, _('try to sort changesets by branches')),
39 43 ('', 'datesort', None, _('try to sort changesets by date')),
40 44 ('', 'sourcesort', None, _('preserve source changesets order')),
41 45 ('', 'closesort', None, _('try to reorder closed revisions'))],
42 46 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
43 47 norepo=True)
44 48 def convert(ui, src, dest=None, revmapfile=None, **opts):
45 49 """convert a foreign SCM repository to a Mercurial one.
46 50
47 51 Accepted source formats [identifiers]:
48 52
49 53 - Mercurial [hg]
50 54 - CVS [cvs]
51 55 - Darcs [darcs]
52 56 - git [git]
53 57 - Subversion [svn]
54 58 - Monotone [mtn]
55 59 - GNU Arch [gnuarch]
56 60 - Bazaar [bzr]
57 61 - Perforce [p4]
58 62
59 63 Accepted destination formats [identifiers]:
60 64
61 65 - Mercurial [hg]
62 66 - Subversion [svn] (history on branches is not preserved)
63 67
64 68 If no revision is given, all revisions will be converted.
65 69 Otherwise, convert will only import up to the named revision
66 70 (given in a format understood by the source).
67 71
68 72 If no destination directory name is specified, it defaults to the
69 73 basename of the source with ``-hg`` appended. If the destination
70 74 repository doesn't exist, it will be created.
71 75
72 76 By default, all sources except Mercurial will use --branchsort.
73 77 Mercurial uses --sourcesort to preserve original revision numbers
74 78 order. Sort modes have the following effects:
75 79
76 80 --branchsort convert from parent to child revision when possible,
77 81 which means branches are usually converted one after
78 82 the other. It generates more compact repositories.
79 83
80 84 --datesort sort revisions by date. Converted repositories have
81 85 good-looking changelogs but are often an order of
82 86 magnitude larger than the same ones generated by
83 87 --branchsort.
84 88
85 89 --sourcesort try to preserve source revisions order, only
86 90 supported by Mercurial sources.
87 91
88 92 --closesort try to move closed revisions as close as possible
89 93 to parent branches, only supported by Mercurial
90 94 sources.
91 95
92 96 If ``REVMAP`` isn't given, it will be put in a default location
93 97 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
94 98 text file that maps each source commit ID to the destination ID
95 99 for that revision, like so::
96 100
97 101 <source ID> <destination ID>
98 102
99 103 If the file doesn't exist, it's automatically created. It's
100 104 updated on each commit copied, so :hg:`convert` can be interrupted
101 105 and can be run repeatedly to copy new commits.
102 106
103 107 The authormap is a simple text file that maps each source commit
104 108 author to a destination commit author. It is handy for source SCMs
105 109 that use unix logins to identify authors (e.g.: CVS). One line per
106 110 author mapping and the line format is::
107 111
108 112 source author = destination author
109 113
110 114 Empty lines and lines starting with a ``#`` are ignored.
111 115
112 116 The filemap is a file that allows filtering and remapping of files
113 117 and directories. Each line can contain one of the following
114 118 directives::
115 119
116 120 include path/to/file-or-dir
117 121
118 122 exclude path/to/file-or-dir
119 123
120 124 rename path/to/source path/to/destination
121 125
122 126 Comment lines start with ``#``. A specified path matches if it
123 127 equals the full relative name of a file or one of its parent
124 128 directories. The ``include`` or ``exclude`` directive with the
125 129 longest matching path applies, so line order does not matter.
126 130
127 131 The ``include`` directive causes a file, or all files under a
128 132 directory, to be included in the destination repository. The default
129 133 if there are no ``include`` statements is to include everything.
130 134 If there are any ``include`` statements, nothing else is included.
131 135 The ``exclude`` directive causes files or directories to
132 136 be omitted. The ``rename`` directive renames a file or directory if
133 137 it is converted. To rename from a subdirectory into the root of
134 138 the repository, use ``.`` as the path to rename to.
135 139
136 140 ``--full`` will make sure the converted changesets contain exactly
137 141 the right files with the right content. It will make a full
138 142 conversion of all files, not just the ones that have
139 143 changed. Files that already are correct will not be changed. This
140 144 can be used to apply filemap changes when converting
141 145 incrementally. This is currently only supported for Mercurial and
142 146 Subversion.
143 147
144 148 The splicemap is a file that allows insertion of synthetic
145 149 history, letting you specify the parents of a revision. This is
146 150 useful if you want to e.g. give a Subversion merge two parents, or
147 151 graft two disconnected series of history together. Each entry
148 152 contains a key, followed by a space, followed by one or two
149 153 comma-separated values::
150 154
151 155 key parent1, parent2
152 156
153 157 The key is the revision ID in the source
154 158 revision control system whose parents should be modified (same
155 159 format as a key in .hg/shamap). The values are the revision IDs
156 160 (in either the source or destination revision control system) that
157 161 should be used as the new parents for that node. For example, if
158 162 you have merged "release-1.0" into "trunk", then you should
159 163 specify the revision on "trunk" as the first parent and the one on
160 164 the "release-1.0" branch as the second.
161 165
162 166 The branchmap is a file that allows you to rename a branch when it is
163 167 being brought in from whatever external repository. When used in
164 168 conjunction with a splicemap, it allows for a powerful combination
165 169 to help fix even the most badly mismanaged repositories and turn them
166 170 into nicely structured Mercurial repositories. The branchmap contains
167 171 lines of the form::
168 172
169 173 original_branch_name new_branch_name
170 174
171 175 where "original_branch_name" is the name of the branch in the
172 176 source repository, and "new_branch_name" is the name of the branch
173 177 is the destination repository. No whitespace is allowed in the
174 178 branch names. This can be used to (for instance) move code in one
175 179 repository from "default" to a named branch.
176 180
177 181 Mercurial Source
178 182 ################
179 183
180 184 The Mercurial source recognizes the following configuration
181 185 options, which you can set on the command line with ``--config``:
182 186
183 187 :convert.hg.ignoreerrors: ignore integrity errors when reading.
184 188 Use it to fix Mercurial repositories with missing revlogs, by
185 189 converting from and to Mercurial. Default is False.
186 190
187 191 :convert.hg.saverev: store original revision ID in changeset
188 192 (forces target IDs to change). It takes a boolean argument and
189 193 defaults to False.
190 194
191 195 :convert.hg.revs: revset specifying the source revisions to convert.
192 196
193 197 CVS Source
194 198 ##########
195 199
196 200 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
197 201 to indicate the starting point of what will be converted. Direct
198 202 access to the repository files is not needed, unless of course the
199 203 repository is ``:local:``. The conversion uses the top level
200 204 directory in the sandbox to find the CVS repository, and then uses
201 205 CVS rlog commands to find files to convert. This means that unless
202 206 a filemap is given, all files under the starting directory will be
203 207 converted, and that any directory reorganization in the CVS
204 208 sandbox is ignored.
205 209
206 210 The following options can be used with ``--config``:
207 211
208 212 :convert.cvsps.cache: Set to False to disable remote log caching,
209 213 for testing and debugging purposes. Default is True.
210 214
211 215 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
212 216 allowed between commits with identical user and log message in
213 217 a single changeset. When very large files were checked in as
214 218 part of a changeset then the default may not be long enough.
215 219 The default is 60.
216 220
217 221 :convert.cvsps.mergeto: Specify a regular expression to which
218 222 commit log messages are matched. If a match occurs, then the
219 223 conversion process will insert a dummy revision merging the
220 224 branch on which this log message occurs to the branch
221 225 indicated in the regex. Default is ``{{mergetobranch
222 226 ([-\\w]+)}}``
223 227
224 228 :convert.cvsps.mergefrom: Specify a regular expression to which
225 229 commit log messages are matched. If a match occurs, then the
226 230 conversion process will add the most recent revision on the
227 231 branch indicated in the regex as the second parent of the
228 232 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
229 233
230 234 :convert.localtimezone: use local time (as determined by the TZ
231 235 environment variable) for changeset date/times. The default
232 236 is False (use UTC).
233 237
234 238 :hooks.cvslog: Specify a Python function to be called at the end of
235 239 gathering the CVS log. The function is passed a list with the
236 240 log entries, and can modify the entries in-place, or add or
237 241 delete them.
238 242
239 243 :hooks.cvschangesets: Specify a Python function to be called after
240 244 the changesets are calculated from the CVS log. The
241 245 function is passed a list with the changeset entries, and can
242 246 modify the changesets in-place, or add or delete them.
243 247
244 248 An additional "debugcvsps" Mercurial command allows the builtin
245 249 changeset merging code to be run without doing a conversion. Its
246 250 parameters and output are similar to that of cvsps 2.1. Please see
247 251 the command help for more details.
248 252
249 253 Subversion Source
250 254 #################
251 255
252 256 Subversion source detects classical trunk/branches/tags layouts.
253 257 By default, the supplied ``svn://repo/path/`` source URL is
254 258 converted as a single branch. If ``svn://repo/path/trunk`` exists
255 259 it replaces the default branch. If ``svn://repo/path/branches``
256 260 exists, its subdirectories are listed as possible branches. If
257 261 ``svn://repo/path/tags`` exists, it is looked for tags referencing
258 262 converted branches. Default ``trunk``, ``branches`` and ``tags``
259 263 values can be overridden with following options. Set them to paths
260 264 relative to the source URL, or leave them blank to disable auto
261 265 detection.
262 266
263 267 The following options can be set with ``--config``:
264 268
265 269 :convert.svn.branches: specify the directory containing branches.
266 270 The default is ``branches``.
267 271
268 272 :convert.svn.tags: specify the directory containing tags. The
269 273 default is ``tags``.
270 274
271 275 :convert.svn.trunk: specify the name of the trunk branch. The
272 276 default is ``trunk``.
273 277
274 278 :convert.localtimezone: use local time (as determined by the TZ
275 279 environment variable) for changeset date/times. The default
276 280 is False (use UTC).
277 281
278 282 Source history can be retrieved starting at a specific revision,
279 283 instead of being integrally converted. Only single branch
280 284 conversions are supported.
281 285
282 286 :convert.svn.startrev: specify start Subversion revision number.
283 287 The default is 0.
284 288
285 289 Git Source
286 290 ##########
287 291
288 292 The Git importer converts commits from all reachable branches (refs
289 293 in refs/heads) and remotes (refs in refs/remotes) to Mercurial.
290 294 Branches are converted to bookmarks with the same name, with the
291 295 leading 'refs/heads' stripped. Git submodules are converted to Git
292 296 subrepos in Mercurial.
293 297
294 298 The following options can be set with ``--config``:
295 299
296 300 :convert.git.similarity: specify how similar files modified in a
297 301 commit must be to be imported as renames or copies, as a
298 302 percentage between ``0`` (disabled) and ``100`` (files must be
299 303 identical). For example, ``90`` means that a delete/add pair will
300 304 be imported as a rename if more than 90% of the file hasn't
301 305 changed. The default is ``50``.
302 306
303 307 :convert.git.findcopiesharder: while detecting copies, look at all
304 308 files in the working copy instead of just changed ones. This
305 309 is very expensive for large projects, and is only effective when
306 310 ``convert.git.similarity`` is greater than 0. The default is False.
307 311
308 312 Perforce Source
309 313 ###############
310 314
311 315 The Perforce (P4) importer can be given a p4 depot path or a
312 316 client specification as source. It will convert all files in the
313 317 source to a flat Mercurial repository, ignoring labels, branches
314 318 and integrations. Note that when a depot path is given you then
315 319 usually should specify a target directory, because otherwise the
316 320 target may be named ``...-hg``.
317 321
318 322 It is possible to limit the amount of source history to be
319 323 converted by specifying an initial Perforce revision:
320 324
321 325 :convert.p4.startrev: specify initial Perforce revision (a
322 326 Perforce changelist number).
323 327
324 328 Mercurial Destination
325 329 #####################
326 330
327 331 The following options are supported:
328 332
329 333 :convert.hg.clonebranches: dispatch source branches in separate
330 334 clones. The default is False.
331 335
332 336 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
333 337 ``default``.
334 338
335 339 :convert.hg.usebranchnames: preserve branch names. The default is
336 340 True.
337 341 """
338 342 return convcmd.convert(ui, src, dest, revmapfile, **opts)
339 343
340 344 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
341 345 def debugsvnlog(ui, **opts):
342 346 return subversion.debugsvnlog(ui, **opts)
343 347
344 348 @command('debugcvsps',
345 349 [
346 350 # Main options shared with cvsps-2.1
347 351 ('b', 'branches', [], _('only return changes on specified branches')),
348 352 ('p', 'prefix', '', _('prefix to remove from file names')),
349 353 ('r', 'revisions', [],
350 354 _('only return changes after or between specified tags')),
351 355 ('u', 'update-cache', None, _("update cvs log cache")),
352 356 ('x', 'new-cache', None, _("create new cvs log cache")),
353 357 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
354 358 ('', 'root', '', _('specify cvsroot')),
355 359 # Options specific to builtin cvsps
356 360 ('', 'parents', '', _('show parent changesets')),
357 361 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
358 362 # Options that are ignored for compatibility with cvsps-2.1
359 363 ('A', 'cvs-direct', None, _('ignored for compatibility')),
360 364 ],
361 365 _('hg debugcvsps [OPTION]... [PATH]...'),
362 366 norepo=True)
363 367 def debugcvsps(ui, *args, **opts):
364 368 '''create changeset information from CVS
365 369
366 370 This command is intended as a debugging tool for the CVS to
367 371 Mercurial converter, and can be used as a direct replacement for
368 372 cvsps.
369 373
370 374 Hg debugcvsps reads the CVS rlog for current directory (or any
371 375 named directory) in the CVS repository, and converts the log to a
372 376 series of changesets based on matching commit log entries and
373 377 dates.'''
374 378 return cvsps.debugcvsps(ui, *args, **opts)
375 379
376 380 def kwconverted(ctx, name):
377 381 rev = ctx.extra().get('convert_revision', '')
378 382 if rev.startswith('svn:'):
379 383 if name == 'svnrev':
380 384 return str(subversion.revsplit(rev)[2])
381 385 elif name == 'svnpath':
382 386 return subversion.revsplit(rev)[1]
383 387 elif name == 'svnuuid':
384 388 return subversion.revsplit(rev)[0]
385 389 return rev
386 390
387 391 def kwsvnrev(repo, ctx, **args):
388 392 """:svnrev: String. Converted subversion revision number."""
389 393 return kwconverted(ctx, 'svnrev')
390 394
391 395 def kwsvnpath(repo, ctx, **args):
392 396 """:svnpath: String. Converted subversion revision project path."""
393 397 return kwconverted(ctx, 'svnpath')
394 398
395 399 def kwsvnuuid(repo, ctx, **args):
396 400 """:svnuuid: String. Converted subversion revision repository identifier."""
397 401 return kwconverted(ctx, 'svnuuid')
398 402
399 403 def extsetup(ui):
400 404 templatekw.keywords['svnrev'] = kwsvnrev
401 405 templatekw.keywords['svnpath'] = kwsvnpath
402 406 templatekw.keywords['svnuuid'] = kwsvnuuid
403 407
404 408 # tell hggettext to extract docstrings from these functions:
405 409 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
@@ -1,350 +1,354 b''
1 1 """automatically manage newlines in repository files
2 2
3 3 This extension allows you to manage the type of line endings (CRLF or
4 4 LF) that are used in the repository and in the local working
5 5 directory. That way you can get CRLF line endings on Windows and LF on
6 6 Unix/Mac, thereby letting everybody use their OS native line endings.
7 7
8 8 The extension reads its configuration from a versioned ``.hgeol``
9 9 configuration file found in the root of the working directory. The
10 10 ``.hgeol`` file use the same syntax as all other Mercurial
11 11 configuration files. It uses two sections, ``[patterns]`` and
12 12 ``[repository]``.
13 13
14 14 The ``[patterns]`` section specifies how line endings should be
15 15 converted between the working directory and the repository. The format is
16 16 specified by a file pattern. The first match is used, so put more
17 17 specific patterns first. The available line endings are ``LF``,
18 18 ``CRLF``, and ``BIN``.
19 19
20 20 Files with the declared format of ``CRLF`` or ``LF`` are always
21 21 checked out and stored in the repository in that format and files
22 22 declared to be binary (``BIN``) are left unchanged. Additionally,
23 23 ``native`` is an alias for checking out in the platform's default line
24 24 ending: ``LF`` on Unix (including Mac OS X) and ``CRLF`` on
25 25 Windows. Note that ``BIN`` (do nothing to line endings) is Mercurial's
26 26 default behaviour; it is only needed if you need to override a later,
27 27 more general pattern.
28 28
29 29 The optional ``[repository]`` section specifies the line endings to
30 30 use for files stored in the repository. It has a single setting,
31 31 ``native``, which determines the storage line endings for files
32 32 declared as ``native`` in the ``[patterns]`` section. It can be set to
33 33 ``LF`` or ``CRLF``. The default is ``LF``. For example, this means
34 34 that on Windows, files configured as ``native`` (``CRLF`` by default)
35 35 will be converted to ``LF`` when stored in the repository. Files
36 36 declared as ``LF``, ``CRLF``, or ``BIN`` in the ``[patterns]`` section
37 37 are always stored as-is in the repository.
38 38
39 39 Example versioned ``.hgeol`` file::
40 40
41 41 [patterns]
42 42 **.py = native
43 43 **.vcproj = CRLF
44 44 **.txt = native
45 45 Makefile = LF
46 46 **.jpg = BIN
47 47
48 48 [repository]
49 49 native = LF
50 50
51 51 .. note::
52 52
53 53 The rules will first apply when files are touched in the working
54 54 directory, e.g. by updating to null and back to tip to touch all files.
55 55
56 56 The extension uses an optional ``[eol]`` section read from both the
57 57 normal Mercurial configuration files and the ``.hgeol`` file, with the
58 58 latter overriding the former. You can use that section to control the
59 59 overall behavior. There are three settings:
60 60
61 61 - ``eol.native`` (default ``os.linesep``) can be set to ``LF`` or
62 62 ``CRLF`` to override the default interpretation of ``native`` for
63 63 checkout. This can be used with :hg:`archive` on Unix, say, to
64 64 generate an archive where files have line endings for Windows.
65 65
66 66 - ``eol.only-consistent`` (default True) can be set to False to make
67 67 the extension convert files with inconsistent EOLs. Inconsistent
68 68 means that there is both ``CRLF`` and ``LF`` present in the file.
69 69 Such files are normally not touched under the assumption that they
70 70 have mixed EOLs on purpose.
71 71
72 72 - ``eol.fix-trailing-newline`` (default False) can be set to True to
73 73 ensure that converted files end with a EOL character (either ``\\n``
74 74 or ``\\r\\n`` as per the configured patterns).
75 75
76 76 The extension provides ``cleverencode:`` and ``cleverdecode:`` filters
77 77 like the deprecated win32text extension does. This means that you can
78 78 disable win32text and enable eol and your filters will still work. You
79 79 only need to these filters until you have prepared a ``.hgeol`` file.
80 80
81 81 The ``win32text.forbid*`` hooks provided by the win32text extension
82 82 have been unified into a single hook named ``eol.checkheadshook``. The
83 83 hook will lookup the expected line endings from the ``.hgeol`` file,
84 84 which means you must migrate to a ``.hgeol`` file first before using
85 85 the hook. ``eol.checkheadshook`` only checks heads, intermediate
86 86 invalid revisions will be pushed. To forbid them completely, use the
87 87 ``eol.checkallhook`` hook. These hooks are best used as
88 88 ``pretxnchangegroup`` hooks.
89 89
90 90 See :hg:`help patterns` for more information about the glob patterns
91 91 used.
92 92 """
93 93
94 94 from mercurial.i18n import _
95 95 from mercurial import util, config, extensions, match, error
96 96 import re, os
97 97
98 # Note for extension authors: ONLY specify testedwith = 'internal' for
99 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
100 # be specifying the version(s) of Mercurial they are tested with, or
101 # leave the attribute unspecified.
98 102 testedwith = 'internal'
99 103
100 104 # Matches a lone LF, i.e., one that is not part of CRLF.
101 105 singlelf = re.compile('(^|[^\r])\n')
102 106 # Matches a single EOL which can either be a CRLF where repeated CR
103 107 # are removed or a LF. We do not care about old Macintosh files, so a
104 108 # stray CR is an error.
105 109 eolre = re.compile('\r*\n')
106 110
107 111
108 112 def inconsistenteol(data):
109 113 return '\r\n' in data and singlelf.search(data)
110 114
111 115 def tolf(s, params, ui, **kwargs):
112 116 """Filter to convert to LF EOLs."""
113 117 if util.binary(s):
114 118 return s
115 119 if ui.configbool('eol', 'only-consistent', True) and inconsistenteol(s):
116 120 return s
117 121 if (ui.configbool('eol', 'fix-trailing-newline', False)
118 122 and s and s[-1] != '\n'):
119 123 s = s + '\n'
120 124 return eolre.sub('\n', s)
121 125
122 126 def tocrlf(s, params, ui, **kwargs):
123 127 """Filter to convert to CRLF EOLs."""
124 128 if util.binary(s):
125 129 return s
126 130 if ui.configbool('eol', 'only-consistent', True) and inconsistenteol(s):
127 131 return s
128 132 if (ui.configbool('eol', 'fix-trailing-newline', False)
129 133 and s and s[-1] != '\n'):
130 134 s = s + '\n'
131 135 return eolre.sub('\r\n', s)
132 136
133 137 def isbinary(s, params):
134 138 """Filter to do nothing with the file."""
135 139 return s
136 140
137 141 filters = {
138 142 'to-lf': tolf,
139 143 'to-crlf': tocrlf,
140 144 'is-binary': isbinary,
141 145 # The following provide backwards compatibility with win32text
142 146 'cleverencode:': tolf,
143 147 'cleverdecode:': tocrlf
144 148 }
145 149
146 150 class eolfile(object):
147 151 def __init__(self, ui, root, data):
148 152 self._decode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
149 153 self._encode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
150 154
151 155 self.cfg = config.config()
152 156 # Our files should not be touched. The pattern must be
153 157 # inserted first override a '** = native' pattern.
154 158 self.cfg.set('patterns', '.hg*', 'BIN', 'eol')
155 159 # We can then parse the user's patterns.
156 160 self.cfg.parse('.hgeol', data)
157 161
158 162 isrepolf = self.cfg.get('repository', 'native') != 'CRLF'
159 163 self._encode['NATIVE'] = isrepolf and 'to-lf' or 'to-crlf'
160 164 iswdlf = ui.config('eol', 'native', os.linesep) in ('LF', '\n')
161 165 self._decode['NATIVE'] = iswdlf and 'to-lf' or 'to-crlf'
162 166
163 167 include = []
164 168 exclude = []
165 169 for pattern, style in self.cfg.items('patterns'):
166 170 key = style.upper()
167 171 if key == 'BIN':
168 172 exclude.append(pattern)
169 173 else:
170 174 include.append(pattern)
171 175 # This will match the files for which we need to care
172 176 # about inconsistent newlines.
173 177 self.match = match.match(root, '', [], include, exclude)
174 178
175 179 def copytoui(self, ui):
176 180 for pattern, style in self.cfg.items('patterns'):
177 181 key = style.upper()
178 182 try:
179 183 ui.setconfig('decode', pattern, self._decode[key], 'eol')
180 184 ui.setconfig('encode', pattern, self._encode[key], 'eol')
181 185 except KeyError:
182 186 ui.warn(_("ignoring unknown EOL style '%s' from %s\n")
183 187 % (style, self.cfg.source('patterns', pattern)))
184 188 # eol.only-consistent can be specified in ~/.hgrc or .hgeol
185 189 for k, v in self.cfg.items('eol'):
186 190 ui.setconfig('eol', k, v, 'eol')
187 191
188 192 def checkrev(self, repo, ctx, files):
189 193 failed = []
190 194 for f in (files or ctx.files()):
191 195 if f not in ctx:
192 196 continue
193 197 for pattern, style in self.cfg.items('patterns'):
194 198 if not match.match(repo.root, '', [pattern])(f):
195 199 continue
196 200 target = self._encode[style.upper()]
197 201 data = ctx[f].data()
198 202 if (target == "to-lf" and "\r\n" in data
199 203 or target == "to-crlf" and singlelf.search(data)):
200 204 failed.append((str(ctx), target, f))
201 205 break
202 206 return failed
203 207
204 208 def parseeol(ui, repo, nodes):
205 209 try:
206 210 for node in nodes:
207 211 try:
208 212 if node is None:
209 213 # Cannot use workingctx.data() since it would load
210 214 # and cache the filters before we configure them.
211 215 data = repo.wfile('.hgeol').read()
212 216 else:
213 217 data = repo[node]['.hgeol'].data()
214 218 return eolfile(ui, repo.root, data)
215 219 except (IOError, LookupError):
216 220 pass
217 221 except error.ParseError, inst:
218 222 ui.warn(_("warning: ignoring .hgeol file due to parse error "
219 223 "at %s: %s\n") % (inst.args[1], inst.args[0]))
220 224 return None
221 225
222 226 def _checkhook(ui, repo, node, headsonly):
223 227 # Get revisions to check and touched files at the same time
224 228 files = set()
225 229 revs = set()
226 230 for rev in xrange(repo[node].rev(), len(repo)):
227 231 revs.add(rev)
228 232 if headsonly:
229 233 ctx = repo[rev]
230 234 files.update(ctx.files())
231 235 for pctx in ctx.parents():
232 236 revs.discard(pctx.rev())
233 237 failed = []
234 238 for rev in revs:
235 239 ctx = repo[rev]
236 240 eol = parseeol(ui, repo, [ctx.node()])
237 241 if eol:
238 242 failed.extend(eol.checkrev(repo, ctx, files))
239 243
240 244 if failed:
241 245 eols = {'to-lf': 'CRLF', 'to-crlf': 'LF'}
242 246 msgs = []
243 247 for node, target, f in failed:
244 248 msgs.append(_(" %s in %s should not have %s line endings") %
245 249 (f, node, eols[target]))
246 250 raise util.Abort(_("end-of-line check failed:\n") + "\n".join(msgs))
247 251
248 252 def checkallhook(ui, repo, node, hooktype, **kwargs):
249 253 """verify that files have expected EOLs"""
250 254 _checkhook(ui, repo, node, False)
251 255
252 256 def checkheadshook(ui, repo, node, hooktype, **kwargs):
253 257 """verify that files have expected EOLs"""
254 258 _checkhook(ui, repo, node, True)
255 259
256 260 # "checkheadshook" used to be called "hook"
257 261 hook = checkheadshook
258 262
259 263 def preupdate(ui, repo, hooktype, parent1, parent2):
260 264 repo.loadeol([parent1])
261 265 return False
262 266
263 267 def uisetup(ui):
264 268 ui.setconfig('hooks', 'preupdate.eol', preupdate, 'eol')
265 269
266 270 def extsetup(ui):
267 271 try:
268 272 extensions.find('win32text')
269 273 ui.warn(_("the eol extension is incompatible with the "
270 274 "win32text extension\n"))
271 275 except KeyError:
272 276 pass
273 277
274 278
275 279 def reposetup(ui, repo):
276 280 uisetup(repo.ui)
277 281
278 282 if not repo.local():
279 283 return
280 284 for name, fn in filters.iteritems():
281 285 repo.adddatafilter(name, fn)
282 286
283 287 ui.setconfig('patch', 'eol', 'auto', 'eol')
284 288
285 289 class eolrepo(repo.__class__):
286 290
287 291 def loadeol(self, nodes):
288 292 eol = parseeol(self.ui, self, nodes)
289 293 if eol is None:
290 294 return None
291 295 eol.copytoui(self.ui)
292 296 return eol.match
293 297
294 298 def _hgcleardirstate(self):
295 299 self._eolfile = self.loadeol([None, 'tip'])
296 300 if not self._eolfile:
297 301 self._eolfile = util.never
298 302 return
299 303
300 304 try:
301 305 cachemtime = os.path.getmtime(self.join("eol.cache"))
302 306 except OSError:
303 307 cachemtime = 0
304 308
305 309 try:
306 310 eolmtime = os.path.getmtime(self.wjoin(".hgeol"))
307 311 except OSError:
308 312 eolmtime = 0
309 313
310 314 if eolmtime > cachemtime:
311 315 self.ui.debug("eol: detected change in .hgeol\n")
312 316 wlock = None
313 317 try:
314 318 wlock = self.wlock()
315 319 for f in self.dirstate:
316 320 if self.dirstate[f] == 'n':
317 321 # all normal files need to be looked at
318 322 # again since the new .hgeol file might no
319 323 # longer match a file it matched before
320 324 self.dirstate.normallookup(f)
321 325 # Create or touch the cache to update mtime
322 326 self.vfs("eol.cache", "w").close()
323 327 wlock.release()
324 328 except error.LockUnavailable:
325 329 # If we cannot lock the repository and clear the
326 330 # dirstate, then a commit might not see all files
327 331 # as modified. But if we cannot lock the
328 332 # repository, then we can also not make a commit,
329 333 # so ignore the error.
330 334 pass
331 335
332 336 def commitctx(self, ctx, error=False):
333 337 for f in sorted(ctx.added() + ctx.modified()):
334 338 if not self._eolfile(f):
335 339 continue
336 340 fctx = ctx[f]
337 341 if fctx is None:
338 342 continue
339 343 data = fctx.data()
340 344 if util.binary(data):
341 345 # We should not abort here, since the user should
342 346 # be able to say "** = native" to automatically
343 347 # have all non-binary files taken care of.
344 348 continue
345 349 if inconsistenteol(data):
346 350 raise util.Abort(_("inconsistent newline style "
347 351 "in %s\n") % f)
348 352 return super(eolrepo, self).commitctx(ctx, error)
349 353 repo.__class__ = eolrepo
350 354 repo._hgcleardirstate()
@@ -1,340 +1,344 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 The extdiff extension also allows you to configure new diff commands, so
17 17 you do not need to type :hg:`extdiff -p kdiff3` always. ::
18 18
19 19 [extdiff]
20 20 # add new command that runs GNU diff(1) in 'context diff' mode
21 21 cdiff = gdiff -Nprc5
22 22 ## or the old way:
23 23 #cmd.cdiff = gdiff
24 24 #opts.cdiff = -Nprc5
25 25
26 26 # add new command called meld, runs meld (no need to name twice). If
27 27 # the meld executable is not available, the meld tool in [merge-tools]
28 28 # will be used, if available
29 29 meld =
30 30
31 31 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
32 32 # (see http://www.vim.org/scripts/script.php?script_id=102) Non
33 33 # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
34 34 # your .vimrc
35 35 vimdiff = gvim -f "+next" \\
36 36 "+execute 'DirDiff' fnameescape(argv(0)) fnameescape(argv(1))"
37 37
38 38 Tool arguments can include variables that are expanded at runtime::
39 39
40 40 $parent1, $plabel1 - filename, descriptive label of first parent
41 41 $child, $clabel - filename, descriptive label of child revision
42 42 $parent2, $plabel2 - filename, descriptive label of second parent
43 43 $root - repository root
44 44 $parent is an alias for $parent1.
45 45
46 46 The extdiff extension will look in your [diff-tools] and [merge-tools]
47 47 sections for diff tool arguments, when none are specified in [extdiff].
48 48
49 49 ::
50 50
51 51 [extdiff]
52 52 kdiff3 =
53 53
54 54 [diff-tools]
55 55 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
56 56
57 57 You can use -I/-X and list of file or directory names like normal
58 58 :hg:`diff` command. The extdiff extension makes snapshots of only
59 59 needed files, so running the external diff program will actually be
60 60 pretty fast (at least faster than having to compare the entire tree).
61 61 '''
62 62
63 63 from mercurial.i18n import _
64 64 from mercurial.node import short, nullid
65 65 from mercurial import cmdutil, scmutil, util, commands, encoding, filemerge
66 66 import os, shlex, shutil, tempfile, re
67 67
68 68 cmdtable = {}
69 69 command = cmdutil.command(cmdtable)
70 # Note for extension authors: ONLY specify testedwith = 'internal' for
71 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
72 # be specifying the version(s) of Mercurial they are tested with, or
73 # leave the attribute unspecified.
70 74 testedwith = 'internal'
71 75
72 76 def snapshot(ui, repo, files, node, tmproot):
73 77 '''snapshot files as of some revision
74 78 if not using snapshot, -I/-X does not work and recursive diff
75 79 in tools like kdiff3 and meld displays too many files.'''
76 80 dirname = os.path.basename(repo.root)
77 81 if dirname == "":
78 82 dirname = "root"
79 83 if node is not None:
80 84 dirname = '%s.%s' % (dirname, short(node))
81 85 base = os.path.join(tmproot, dirname)
82 86 os.mkdir(base)
83 87 if node is not None:
84 88 ui.note(_('making snapshot of %d files from rev %s\n') %
85 89 (len(files), short(node)))
86 90 else:
87 91 ui.note(_('making snapshot of %d files from working directory\n') %
88 92 (len(files)))
89 93 wopener = scmutil.opener(base)
90 94 fns_and_mtime = []
91 95 ctx = repo[node]
92 96 for fn in sorted(files):
93 97 wfn = util.pconvert(fn)
94 98 if wfn not in ctx:
95 99 # File doesn't exist; could be a bogus modify
96 100 continue
97 101 ui.note(' %s\n' % wfn)
98 102 dest = os.path.join(base, wfn)
99 103 fctx = ctx[wfn]
100 104 data = repo.wwritedata(wfn, fctx.data())
101 105 if 'l' in fctx.flags():
102 106 wopener.symlink(data, wfn)
103 107 else:
104 108 wopener.write(wfn, data)
105 109 if 'x' in fctx.flags():
106 110 util.setflags(dest, False, True)
107 111 if node is None:
108 112 fns_and_mtime.append((dest, repo.wjoin(fn),
109 113 os.lstat(dest).st_mtime))
110 114 return dirname, fns_and_mtime
111 115
112 116 def dodiff(ui, repo, cmdline, pats, opts):
113 117 '''Do the actual diff:
114 118
115 119 - copy to a temp structure if diffing 2 internal revisions
116 120 - copy to a temp structure if diffing working revision with
117 121 another one and more than 1 file is changed
118 122 - just invoke the diff for a single file in the working dir
119 123 '''
120 124
121 125 revs = opts.get('rev')
122 126 change = opts.get('change')
123 127 do3way = '$parent2' in cmdline
124 128
125 129 if revs and change:
126 130 msg = _('cannot specify --rev and --change at the same time')
127 131 raise util.Abort(msg)
128 132 elif change:
129 133 node2 = scmutil.revsingle(repo, change, None).node()
130 134 node1a, node1b = repo.changelog.parents(node2)
131 135 else:
132 136 node1a, node2 = scmutil.revpair(repo, revs)
133 137 if not revs:
134 138 node1b = repo.dirstate.p2()
135 139 else:
136 140 node1b = nullid
137 141
138 142 # Disable 3-way merge if there is only one parent
139 143 if do3way:
140 144 if node1b == nullid:
141 145 do3way = False
142 146
143 147 matcher = scmutil.match(repo[node2], pats, opts)
144 148 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher)[:3])
145 149 if do3way:
146 150 mod_b, add_b, rem_b = map(set, repo.status(node1b, node2, matcher)[:3])
147 151 else:
148 152 mod_b, add_b, rem_b = set(), set(), set()
149 153 modadd = mod_a | add_a | mod_b | add_b
150 154 common = modadd | rem_a | rem_b
151 155 if not common:
152 156 return 0
153 157
154 158 tmproot = tempfile.mkdtemp(prefix='extdiff.')
155 159 try:
156 160 # Always make a copy of node1a (and node1b, if applicable)
157 161 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
158 162 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot)[0]
159 163 rev1a = '@%d' % repo[node1a].rev()
160 164 if do3way:
161 165 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
162 166 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot)[0]
163 167 rev1b = '@%d' % repo[node1b].rev()
164 168 else:
165 169 dir1b = None
166 170 rev1b = ''
167 171
168 172 fns_and_mtime = []
169 173
170 174 # If node2 in not the wc or there is >1 change, copy it
171 175 dir2root = ''
172 176 rev2 = ''
173 177 if node2:
174 178 dir2 = snapshot(ui, repo, modadd, node2, tmproot)[0]
175 179 rev2 = '@%d' % repo[node2].rev()
176 180 elif len(common) > 1:
177 181 #we only actually need to get the files to copy back to
178 182 #the working dir in this case (because the other cases
179 183 #are: diffing 2 revisions or single file -- in which case
180 184 #the file is already directly passed to the diff tool).
181 185 dir2, fns_and_mtime = snapshot(ui, repo, modadd, None, tmproot)
182 186 else:
183 187 # This lets the diff tool open the changed file directly
184 188 dir2 = ''
185 189 dir2root = repo.root
186 190
187 191 label1a = rev1a
188 192 label1b = rev1b
189 193 label2 = rev2
190 194
191 195 # If only one change, diff the files instead of the directories
192 196 # Handle bogus modifies correctly by checking if the files exist
193 197 if len(common) == 1:
194 198 common_file = util.localpath(common.pop())
195 199 dir1a = os.path.join(tmproot, dir1a, common_file)
196 200 label1a = common_file + rev1a
197 201 if not os.path.isfile(dir1a):
198 202 dir1a = os.devnull
199 203 if do3way:
200 204 dir1b = os.path.join(tmproot, dir1b, common_file)
201 205 label1b = common_file + rev1b
202 206 if not os.path.isfile(dir1b):
203 207 dir1b = os.devnull
204 208 dir2 = os.path.join(dir2root, dir2, common_file)
205 209 label2 = common_file + rev2
206 210
207 211 # Function to quote file/dir names in the argument string.
208 212 # When not operating in 3-way mode, an empty string is
209 213 # returned for parent2
210 214 replace = {'parent': dir1a, 'parent1': dir1a, 'parent2': dir1b,
211 215 'plabel1': label1a, 'plabel2': label1b,
212 216 'clabel': label2, 'child': dir2,
213 217 'root': repo.root}
214 218 def quote(match):
215 219 pre = match.group(2)
216 220 key = match.group(3)
217 221 if not do3way and key == 'parent2':
218 222 return pre
219 223 return pre + util.shellquote(replace[key])
220 224
221 225 # Match parent2 first, so 'parent1?' will match both parent1 and parent
222 226 regex = (r'''(['"]?)([^\s'"$]*)'''
223 227 r'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1')
224 228 if not do3way and not re.search(regex, cmdline):
225 229 cmdline += ' $parent1 $child'
226 230 cmdline = re.sub(regex, quote, cmdline)
227 231
228 232 ui.debug('running %r in %s\n' % (cmdline, tmproot))
229 233 ui.system(cmdline, cwd=tmproot)
230 234
231 235 for copy_fn, working_fn, mtime in fns_and_mtime:
232 236 if os.lstat(copy_fn).st_mtime != mtime:
233 237 ui.debug('file changed while diffing. '
234 238 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
235 239 util.copyfile(copy_fn, working_fn)
236 240
237 241 return 1
238 242 finally:
239 243 ui.note(_('cleaning up temp directory\n'))
240 244 shutil.rmtree(tmproot)
241 245
242 246 @command('extdiff',
243 247 [('p', 'program', '',
244 248 _('comparison program to run'), _('CMD')),
245 249 ('o', 'option', [],
246 250 _('pass option to comparison program'), _('OPT')),
247 251 ('r', 'rev', [], _('revision'), _('REV')),
248 252 ('c', 'change', '', _('change made by revision'), _('REV')),
249 253 ] + commands.walkopts,
250 254 _('hg extdiff [OPT]... [FILE]...'),
251 255 inferrepo=True)
252 256 def extdiff(ui, repo, *pats, **opts):
253 257 '''use external program to diff repository (or selected files)
254 258
255 259 Show differences between revisions for the specified files, using
256 260 an external program. The default program used is diff, with
257 261 default options "-Npru".
258 262
259 263 To select a different program, use the -p/--program option. The
260 264 program will be passed the names of two directories to compare. To
261 265 pass additional options to the program, use -o/--option. These
262 266 will be passed before the names of the directories to compare.
263 267
264 268 When two revision arguments are given, then changes are shown
265 269 between those revisions. If only one revision is specified then
266 270 that revision is compared to the working directory, and, when no
267 271 revisions are specified, the working directory files are compared
268 272 to its parent.'''
269 273 program = opts.get('program')
270 274 option = opts.get('option')
271 275 if not program:
272 276 program = 'diff'
273 277 option = option or ['-Npru']
274 278 cmdline = ' '.join(map(util.shellquote, [program] + option))
275 279 return dodiff(ui, repo, cmdline, pats, opts)
276 280
277 281 def uisetup(ui):
278 282 for cmd, path in ui.configitems('extdiff'):
279 283 path = util.expandpath(path)
280 284 if cmd.startswith('cmd.'):
281 285 cmd = cmd[4:]
282 286 if not path:
283 287 path = util.findexe(cmd)
284 288 if path is None:
285 289 path = filemerge.findexternaltool(ui, cmd) or cmd
286 290 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
287 291 cmdline = util.shellquote(path)
288 292 if diffopts:
289 293 cmdline += ' ' + diffopts
290 294 elif cmd.startswith('opts.'):
291 295 continue
292 296 else:
293 297 if path:
294 298 # case "cmd = path opts"
295 299 cmdline = path
296 300 diffopts = len(shlex.split(cmdline)) > 1
297 301 else:
298 302 # case "cmd ="
299 303 path = util.findexe(cmd)
300 304 if path is None:
301 305 path = filemerge.findexternaltool(ui, cmd) or cmd
302 306 cmdline = util.shellquote(path)
303 307 diffopts = False
304 308 # look for diff arguments in [diff-tools] then [merge-tools]
305 309 if not diffopts:
306 310 args = ui.config('diff-tools', cmd+'.diffargs') or \
307 311 ui.config('merge-tools', cmd+'.diffargs')
308 312 if args:
309 313 cmdline += ' ' + args
310 314 def save(cmdline):
311 315 '''use closure to save diff command to use'''
312 316 def mydiff(ui, repo, *pats, **opts):
313 317 options = ' '.join(map(util.shellquote, opts['option']))
314 318 if options:
315 319 options = ' ' + options
316 320 return dodiff(ui, repo, cmdline + options, pats, opts)
317 321 doc = _('''\
318 322 use %(path)s to diff repository (or selected files)
319 323
320 324 Show differences between revisions for the specified files, using
321 325 the %(path)s program.
322 326
323 327 When two revision arguments are given, then changes are shown
324 328 between those revisions. If only one revision is specified then
325 329 that revision is compared to the working directory, and, when no
326 330 revisions are specified, the working directory files are compared
327 331 to its parent.\
328 332 ''') % {'path': util.uirepr(path)}
329 333
330 334 # We must translate the docstring right away since it is
331 335 # used as a format string. The string will unfortunately
332 336 # be translated again in commands.helpcmd and this will
333 337 # fail when the docstring contains non-ASCII characters.
334 338 # Decoding the string to a Unicode string here (using the
335 339 # right encoding) prevents that.
336 340 mydiff.__doc__ = doc.decode(encoding.encoding)
337 341 return mydiff
338 342 cmdtable[cmd] = (save(cmdline),
339 343 cmdtable['extdiff'][1][1:],
340 344 _('hg %s [OPTION]... [FILE]...') % cmd)
@@ -1,150 +1,154 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 mercurial.i18n import _
11 11 from mercurial.node import short
12 12 from mercurial import commands, cmdutil, hg, util, error
13 13 from mercurial.lock import release
14 14 from mercurial import exchange
15 15
16 16 cmdtable = {}
17 17 command = cmdutil.command(cmdtable)
18 # Note for extension authors: ONLY specify testedwith = 'internal' for
19 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
20 # be specifying the version(s) of Mercurial they are tested with, or
21 # leave the attribute unspecified.
18 22 testedwith = 'internal'
19 23
20 24 @command('fetch',
21 25 [('r', 'rev', [],
22 26 _('a specific revision you would like to pull'), _('REV')),
23 27 ('e', 'edit', None, _('invoke editor on commit messages')),
24 28 ('', 'force-editor', None, _('edit commit message (DEPRECATED)')),
25 29 ('', 'switch-parent', None, _('switch parents when merging')),
26 30 ] + commands.commitopts + commands.commitopts2 + commands.remoteopts,
27 31 _('hg fetch [SOURCE]'))
28 32 def fetch(ui, repo, source='default', **opts):
29 33 '''pull changes from a remote repository, merge new changes if needed.
30 34
31 35 This finds all changes from the repository at the specified path
32 36 or URL and adds them to the local repository.
33 37
34 38 If the pulled changes add a new branch head, the head is
35 39 automatically merged, and the result of the merge is committed.
36 40 Otherwise, the working directory is updated to include the new
37 41 changes.
38 42
39 43 When a merge is needed, the working directory is first updated to
40 44 the newly pulled changes. Local changes are then merged into the
41 45 pulled changes. To switch the merge order, use --switch-parent.
42 46
43 47 See :hg:`help dates` for a list of formats valid for -d/--date.
44 48
45 49 Returns 0 on success.
46 50 '''
47 51
48 52 date = opts.get('date')
49 53 if date:
50 54 opts['date'] = util.parsedate(date)
51 55
52 56 parent, _p2 = repo.dirstate.parents()
53 57 branch = repo.dirstate.branch()
54 58 try:
55 59 branchnode = repo.branchtip(branch)
56 60 except error.RepoLookupError:
57 61 branchnode = None
58 62 if parent != branchnode:
59 63 raise util.Abort(_('working directory not at branch tip'),
60 64 hint=_('use "hg update" to check out branch tip'))
61 65
62 66 wlock = lock = None
63 67 try:
64 68 wlock = repo.wlock()
65 69 lock = repo.lock()
66 70
67 71 cmdutil.bailifchanged(repo)
68 72
69 73 bheads = repo.branchheads(branch)
70 74 bheads = [head for head in bheads if len(repo[head].children()) == 0]
71 75 if len(bheads) > 1:
72 76 raise util.Abort(_('multiple heads in this branch '
73 77 '(use "hg heads ." and "hg merge" to merge)'))
74 78
75 79 other = hg.peer(repo, opts, ui.expandpath(source))
76 80 ui.status(_('pulling from %s\n') %
77 81 util.hidepassword(ui.expandpath(source)))
78 82 revs = None
79 83 if opts['rev']:
80 84 try:
81 85 revs = [other.lookup(rev) for rev in opts['rev']]
82 86 except error.CapabilityError:
83 87 err = _("other repository doesn't support revision lookup, "
84 88 "so a rev cannot be specified.")
85 89 raise util.Abort(err)
86 90
87 91 # Are there any changes at all?
88 92 modheads = exchange.pull(repo, other, heads=revs).cgresult
89 93 if modheads == 0:
90 94 return 0
91 95
92 96 # Is this a simple fast-forward along the current branch?
93 97 newheads = repo.branchheads(branch)
94 98 newchildren = repo.changelog.nodesbetween([parent], newheads)[2]
95 99 if len(newheads) == 1 and len(newchildren):
96 100 if newchildren[0] != parent:
97 101 return hg.update(repo, newchildren[0])
98 102 else:
99 103 return 0
100 104
101 105 # Are there more than one additional branch heads?
102 106 newchildren = [n for n in newchildren if n != parent]
103 107 newparent = parent
104 108 if newchildren:
105 109 newparent = newchildren[0]
106 110 hg.clean(repo, newparent)
107 111 newheads = [n for n in newheads if n != newparent]
108 112 if len(newheads) > 1:
109 113 ui.status(_('not merging with %d other new branch heads '
110 114 '(use "hg heads ." and "hg merge" to merge them)\n') %
111 115 (len(newheads) - 1))
112 116 return 1
113 117
114 118 if not newheads:
115 119 return 0
116 120
117 121 # Otherwise, let's merge.
118 122 err = False
119 123 if newheads:
120 124 # By default, we consider the repository we're pulling
121 125 # *from* as authoritative, so we merge our changes into
122 126 # theirs.
123 127 if opts['switch_parent']:
124 128 firstparent, secondparent = newparent, newheads[0]
125 129 else:
126 130 firstparent, secondparent = newheads[0], newparent
127 131 ui.status(_('updating to %d:%s\n') %
128 132 (repo.changelog.rev(firstparent),
129 133 short(firstparent)))
130 134 hg.clean(repo, firstparent)
131 135 ui.status(_('merging with %d:%s\n') %
132 136 (repo.changelog.rev(secondparent), short(secondparent)))
133 137 err = hg.merge(repo, secondparent, remind=False)
134 138
135 139 if not err:
136 140 # we don't translate commit messages
137 141 message = (cmdutil.logmessage(ui, opts) or
138 142 ('Automated merge with %s' %
139 143 util.removeauth(other.url())))
140 144 editopt = opts.get('edit') or opts.get('force_editor')
141 145 editor = cmdutil.getcommiteditor(edit=editopt, editform='fetch')
142 146 n = repo.commit(message, opts['user'], opts['date'], editor=editor)
143 147 ui.status(_('new changeset %d:%s merges remote changes '
144 148 'with local\n') % (repo.changelog.rev(n),
145 149 short(n)))
146 150
147 151 return err
148 152
149 153 finally:
150 154 release(lock, wlock)
@@ -1,297 +1,301 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 import os, tempfile, binascii
9 9 from mercurial import util, commands, match, cmdutil
10 10 from mercurial import node as hgnode
11 11 from mercurial.i18n import _
12 12
13 13 cmdtable = {}
14 14 command = cmdutil.command(cmdtable)
15 # Note for extension authors: ONLY specify testedwith = 'internal' for
16 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
17 # be specifying the version(s) of Mercurial they are tested with, or
18 # leave the attribute unspecified.
15 19 testedwith = 'internal'
16 20
17 21 class gpg(object):
18 22 def __init__(self, path, key=None):
19 23 self.path = path
20 24 self.key = (key and " --local-user \"%s\"" % key) or ""
21 25
22 26 def sign(self, data):
23 27 gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
24 28 return util.filter(data, gpgcmd)
25 29
26 30 def verify(self, data, sig):
27 31 """ returns of the good and bad signatures"""
28 32 sigfile = datafile = None
29 33 try:
30 34 # create temporary files
31 35 fd, sigfile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".sig")
32 36 fp = os.fdopen(fd, 'wb')
33 37 fp.write(sig)
34 38 fp.close()
35 39 fd, datafile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".txt")
36 40 fp = os.fdopen(fd, 'wb')
37 41 fp.write(data)
38 42 fp.close()
39 43 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
40 44 "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
41 45 ret = util.filter("", gpgcmd)
42 46 finally:
43 47 for f in (sigfile, datafile):
44 48 try:
45 49 if f:
46 50 os.unlink(f)
47 51 except OSError:
48 52 pass
49 53 keys = []
50 54 key, fingerprint = None, None
51 55 for l in ret.splitlines():
52 56 # see DETAILS in the gnupg documentation
53 57 # filter the logger output
54 58 if not l.startswith("[GNUPG:]"):
55 59 continue
56 60 l = l[9:]
57 61 if l.startswith("VALIDSIG"):
58 62 # fingerprint of the primary key
59 63 fingerprint = l.split()[10]
60 64 elif l.startswith("ERRSIG"):
61 65 key = l.split(" ", 3)[:2]
62 66 key.append("")
63 67 fingerprint = None
64 68 elif (l.startswith("GOODSIG") or
65 69 l.startswith("EXPSIG") or
66 70 l.startswith("EXPKEYSIG") or
67 71 l.startswith("BADSIG")):
68 72 if key is not None:
69 73 keys.append(key + [fingerprint])
70 74 key = l.split(" ", 2)
71 75 fingerprint = None
72 76 if key is not None:
73 77 keys.append(key + [fingerprint])
74 78 return keys
75 79
76 80 def newgpg(ui, **opts):
77 81 """create a new gpg instance"""
78 82 gpgpath = ui.config("gpg", "cmd", "gpg")
79 83 gpgkey = opts.get('key')
80 84 if not gpgkey:
81 85 gpgkey = ui.config("gpg", "key", None)
82 86 return gpg(gpgpath, gpgkey)
83 87
84 88 def sigwalk(repo):
85 89 """
86 90 walk over every sigs, yields a couple
87 91 ((node, version, sig), (filename, linenumber))
88 92 """
89 93 def parsefile(fileiter, context):
90 94 ln = 1
91 95 for l in fileiter:
92 96 if not l:
93 97 continue
94 98 yield (l.split(" ", 2), (context, ln))
95 99 ln += 1
96 100
97 101 # read the heads
98 102 fl = repo.file(".hgsigs")
99 103 for r in reversed(fl.heads()):
100 104 fn = ".hgsigs|%s" % hgnode.short(r)
101 105 for item in parsefile(fl.read(r).splitlines(), fn):
102 106 yield item
103 107 try:
104 108 # read local signatures
105 109 fn = "localsigs"
106 110 for item in parsefile(repo.vfs(fn), fn):
107 111 yield item
108 112 except IOError:
109 113 pass
110 114
111 115 def getkeys(ui, repo, mygpg, sigdata, context):
112 116 """get the keys who signed a data"""
113 117 fn, ln = context
114 118 node, version, sig = sigdata
115 119 prefix = "%s:%d" % (fn, ln)
116 120 node = hgnode.bin(node)
117 121
118 122 data = node2txt(repo, node, version)
119 123 sig = binascii.a2b_base64(sig)
120 124 keys = mygpg.verify(data, sig)
121 125
122 126 validkeys = []
123 127 # warn for expired key and/or sigs
124 128 for key in keys:
125 129 if key[0] == "ERRSIG":
126 130 ui.write(_("%s Unknown key ID \"%s\"\n")
127 131 % (prefix, shortkey(ui, key[1][:15])))
128 132 continue
129 133 if key[0] == "BADSIG":
130 134 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
131 135 continue
132 136 if key[0] == "EXPSIG":
133 137 ui.write(_("%s Note: Signature has expired"
134 138 " (signed by: \"%s\")\n") % (prefix, key[2]))
135 139 elif key[0] == "EXPKEYSIG":
136 140 ui.write(_("%s Note: This key has expired"
137 141 " (signed by: \"%s\")\n") % (prefix, key[2]))
138 142 validkeys.append((key[1], key[2], key[3]))
139 143 return validkeys
140 144
141 145 @command("sigs", [], _('hg sigs'))
142 146 def sigs(ui, repo):
143 147 """list signed changesets"""
144 148 mygpg = newgpg(ui)
145 149 revs = {}
146 150
147 151 for data, context in sigwalk(repo):
148 152 node, version, sig = data
149 153 fn, ln = context
150 154 try:
151 155 n = repo.lookup(node)
152 156 except KeyError:
153 157 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
154 158 continue
155 159 r = repo.changelog.rev(n)
156 160 keys = getkeys(ui, repo, mygpg, data, context)
157 161 if not keys:
158 162 continue
159 163 revs.setdefault(r, [])
160 164 revs[r].extend(keys)
161 165 for rev in sorted(revs, reverse=True):
162 166 for k in revs[rev]:
163 167 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
164 168 ui.write("%-30s %s\n" % (keystr(ui, k), r))
165 169
166 170 @command("sigcheck", [], _('hg sigcheck REV'))
167 171 def check(ui, repo, rev):
168 172 """verify all the signatures there may be for a particular revision"""
169 173 mygpg = newgpg(ui)
170 174 rev = repo.lookup(rev)
171 175 hexrev = hgnode.hex(rev)
172 176 keys = []
173 177
174 178 for data, context in sigwalk(repo):
175 179 node, version, sig = data
176 180 if node == hexrev:
177 181 k = getkeys(ui, repo, mygpg, data, context)
178 182 if k:
179 183 keys.extend(k)
180 184
181 185 if not keys:
182 186 ui.write(_("no valid signature for %s\n") % hgnode.short(rev))
183 187 return
184 188
185 189 # print summary
186 190 ui.write("%s is signed by:\n" % hgnode.short(rev))
187 191 for key in keys:
188 192 ui.write(" %s\n" % keystr(ui, key))
189 193
190 194 def keystr(ui, key):
191 195 """associate a string to a key (username, comment)"""
192 196 keyid, user, fingerprint = key
193 197 comment = ui.config("gpg", fingerprint, None)
194 198 if comment:
195 199 return "%s (%s)" % (user, comment)
196 200 else:
197 201 return user
198 202
199 203 @command("sign",
200 204 [('l', 'local', None, _('make the signature local')),
201 205 ('f', 'force', None, _('sign even if the sigfile is modified')),
202 206 ('', 'no-commit', None, _('do not commit the sigfile after signing')),
203 207 ('k', 'key', '',
204 208 _('the key id to sign with'), _('ID')),
205 209 ('m', 'message', '',
206 210 _('use text as commit message'), _('TEXT')),
207 211 ('e', 'edit', False, _('invoke editor on commit messages')),
208 212 ] + commands.commitopts2,
209 213 _('hg sign [OPTION]... [REV]...'))
210 214 def sign(ui, repo, *revs, **opts):
211 215 """add a signature for the current or given revision
212 216
213 217 If no revision is given, the parent of the working directory is used,
214 218 or tip if no revision is checked out.
215 219
216 220 See :hg:`help dates` for a list of formats valid for -d/--date.
217 221 """
218 222
219 223 mygpg = newgpg(ui, **opts)
220 224 sigver = "0"
221 225 sigmessage = ""
222 226
223 227 date = opts.get('date')
224 228 if date:
225 229 opts['date'] = util.parsedate(date)
226 230
227 231 if revs:
228 232 nodes = [repo.lookup(n) for n in revs]
229 233 else:
230 234 nodes = [node for node in repo.dirstate.parents()
231 235 if node != hgnode.nullid]
232 236 if len(nodes) > 1:
233 237 raise util.Abort(_('uncommitted merge - please provide a '
234 238 'specific revision'))
235 239 if not nodes:
236 240 nodes = [repo.changelog.tip()]
237 241
238 242 for n in nodes:
239 243 hexnode = hgnode.hex(n)
240 244 ui.write(_("signing %d:%s\n") % (repo.changelog.rev(n),
241 245 hgnode.short(n)))
242 246 # build data
243 247 data = node2txt(repo, n, sigver)
244 248 sig = mygpg.sign(data)
245 249 if not sig:
246 250 raise util.Abort(_("error while signing"))
247 251 sig = binascii.b2a_base64(sig)
248 252 sig = sig.replace("\n", "")
249 253 sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
250 254
251 255 # write it
252 256 if opts['local']:
253 257 repo.vfs.append("localsigs", sigmessage)
254 258 return
255 259
256 260 if not opts["force"]:
257 261 msigs = match.exact(repo.root, '', ['.hgsigs'])
258 262 if any(repo.status(match=msigs, unknown=True, ignored=True)):
259 263 raise util.Abort(_("working copy of .hgsigs is changed "),
260 264 hint=_("please commit .hgsigs manually"))
261 265
262 266 sigsfile = repo.wfile(".hgsigs", "ab")
263 267 sigsfile.write(sigmessage)
264 268 sigsfile.close()
265 269
266 270 if '.hgsigs' not in repo.dirstate:
267 271 repo[None].add([".hgsigs"])
268 272
269 273 if opts["no_commit"]:
270 274 return
271 275
272 276 message = opts['message']
273 277 if not message:
274 278 # we don't translate commit messages
275 279 message = "\n".join(["Added signature for changeset %s"
276 280 % hgnode.short(n)
277 281 for n in nodes])
278 282 try:
279 283 editor = cmdutil.getcommiteditor(editform='gpg.sign', **opts)
280 284 repo.commit(message, opts['user'], opts['date'], match=msigs,
281 285 editor=editor)
282 286 except ValueError, inst:
283 287 raise util.Abort(str(inst))
284 288
285 289 def shortkey(ui, key):
286 290 if len(key) != 16:
287 291 ui.debug("key ID \"%s\" format error\n" % key)
288 292 return key
289 293
290 294 return key[-8:]
291 295
292 296 def node2txt(repo, node, ver):
293 297 """map a manifest into some text"""
294 298 if ver == "0":
295 299 return "%s\n" % hgnode.hex(node)
296 300 else:
297 301 raise util.Abort(_("unknown signature version"))
@@ -1,58 +1,62 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.
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 mercurial.i18n import _
19 19 from mercurial import cmdutil, commands
20 20
21 21 cmdtable = {}
22 22 command = cmdutil.command(cmdtable)
23 # Note for extension authors: ONLY specify testedwith = 'internal' for
24 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
25 # be specifying the version(s) of Mercurial they are tested with, or
26 # leave the attribute unspecified.
23 27 testedwith = 'internal'
24 28
25 29 @command('glog',
26 30 [('f', 'follow', None,
27 31 _('follow changeset history, or file history across copies and renames')),
28 32 ('', 'follow-first', None,
29 33 _('only follow the first parent of merge changesets (DEPRECATED)')),
30 34 ('d', 'date', '', _('show revisions matching date spec'), _('DATE')),
31 35 ('C', 'copies', None, _('show copied files')),
32 36 ('k', 'keyword', [],
33 37 _('do case-insensitive search for a given text'), _('TEXT')),
34 38 ('r', 'rev', [], _('show the specified revision or revset'), _('REV')),
35 39 ('', 'removed', None, _('include revisions where files were removed')),
36 40 ('m', 'only-merges', None, _('show only merges (DEPRECATED)')),
37 41 ('u', 'user', [], _('revisions committed by user'), _('USER')),
38 42 ('', 'only-branch', [],
39 43 _('show only changesets within the given named branch (DEPRECATED)'),
40 44 _('BRANCH')),
41 45 ('b', 'branch', [],
42 46 _('show changesets within the given named branch'), _('BRANCH')),
43 47 ('P', 'prune', [],
44 48 _('do not display revision or any of its ancestors'), _('REV')),
45 49 ] + commands.logopts + commands.walkopts,
46 50 _('[OPTION]... [FILE]'),
47 51 inferrepo=True)
48 52 def graphlog(ui, repo, *pats, **opts):
49 53 """show revision history alongside an ASCII revision graph
50 54
51 55 Print a revision history alongside a revision graph drawn with
52 56 ASCII characters.
53 57
54 58 Nodes printed as an @ character are parents of the working
55 59 directory.
56 60 """
57 61 opts['graph'] = True
58 62 return commands.log(ui, repo, *pats, **opts)
@@ -1,281 +1,285 b''
1 1 # Copyright (C) 2007-8 Brendan Cully <brendan@kublai.com>
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 """hooks for integrating with the CIA.vc notification service
7 7
8 8 This is meant to be run as a changegroup or incoming hook. To
9 9 configure it, set the following options in your hgrc::
10 10
11 11 [cia]
12 12 # your registered CIA user name
13 13 user = foo
14 14 # the name of the project in CIA
15 15 project = foo
16 16 # the module (subproject) (optional)
17 17 #module = foo
18 18 # Append a diffstat to the log message (optional)
19 19 #diffstat = False
20 20 # Template to use for log messages (optional)
21 21 #template = {desc}\\n{baseurl}{webroot}/rev/{node}-- {diffstat}
22 22 # Style to use (optional)
23 23 #style = foo
24 24 # The URL of the CIA notification service (optional)
25 25 # You can use mailto: URLs to send by email, e.g.
26 26 # mailto:cia@cia.vc
27 27 # Make sure to set email.from if you do this.
28 28 #url = http://cia.vc/
29 29 # print message instead of sending it (optional)
30 30 #test = False
31 31 # number of slashes to strip for url paths
32 32 #strip = 0
33 33
34 34 [hooks]
35 35 # one of these:
36 36 changegroup.cia = python:hgcia.hook
37 37 #incoming.cia = python:hgcia.hook
38 38
39 39 [web]
40 40 # If you want hyperlinks (optional)
41 41 baseurl = http://server/path/to/repo
42 42 """
43 43
44 44 from mercurial.i18n import _
45 45 from mercurial.node import bin, short
46 46 from mercurial import cmdutil, patch, util, mail
47 47 import email.Parser
48 48
49 49 import socket, xmlrpclib
50 50 from xml.sax import saxutils
51 # Note for extension authors: ONLY specify testedwith = 'internal' for
52 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
53 # be specifying the version(s) of Mercurial they are tested with, or
54 # leave the attribute unspecified.
51 55 testedwith = 'internal'
52 56
53 57 socket_timeout = 30 # seconds
54 58 if util.safehasattr(socket, 'setdefaulttimeout'):
55 59 # set a timeout for the socket so you don't have to wait so looooong
56 60 # when cia.vc is having problems. requires python >= 2.3:
57 61 socket.setdefaulttimeout(socket_timeout)
58 62
59 63 HGCIA_VERSION = '0.1'
60 64 HGCIA_URL = 'http://hg.kublai.com/mercurial/hgcia'
61 65
62 66
63 67 class ciamsg(object):
64 68 """ A CIA message """
65 69 def __init__(self, cia, ctx):
66 70 self.cia = cia
67 71 self.ctx = ctx
68 72 self.url = self.cia.url
69 73 if self.url:
70 74 self.url += self.cia.root
71 75
72 76 def fileelem(self, path, uri, action):
73 77 if uri:
74 78 uri = ' uri=%s' % saxutils.quoteattr(uri)
75 79 return '<file%s action=%s>%s</file>' % (
76 80 uri, saxutils.quoteattr(action), saxutils.escape(path))
77 81
78 82 def fileelems(self):
79 83 n = self.ctx.node()
80 84 f = self.cia.repo.status(self.ctx.p1().node(), n)
81 85 url = self.url or ''
82 86 if url and url[-1] == '/':
83 87 url = url[:-1]
84 88 elems = []
85 89 for path in f.modified:
86 90 uri = '%s/diff/%s/%s' % (url, short(n), path)
87 91 elems.append(self.fileelem(path, url and uri, 'modify'))
88 92 for path in f.added:
89 93 # TODO: copy/rename ?
90 94 uri = '%s/file/%s/%s' % (url, short(n), path)
91 95 elems.append(self.fileelem(path, url and uri, 'add'))
92 96 for path in f.removed:
93 97 elems.append(self.fileelem(path, '', 'remove'))
94 98
95 99 return '\n'.join(elems)
96 100
97 101 def sourceelem(self, project, module=None, branch=None):
98 102 msg = ['<source>', '<project>%s</project>' % saxutils.escape(project)]
99 103 if module:
100 104 msg.append('<module>%s</module>' % saxutils.escape(module))
101 105 if branch:
102 106 msg.append('<branch>%s</branch>' % saxutils.escape(branch))
103 107 msg.append('</source>')
104 108
105 109 return '\n'.join(msg)
106 110
107 111 def diffstat(self):
108 112 class patchbuf(object):
109 113 def __init__(self):
110 114 self.lines = []
111 115 # diffstat is stupid
112 116 self.name = 'cia'
113 117 def write(self, data):
114 118 self.lines += data.splitlines(True)
115 119 def close(self):
116 120 pass
117 121
118 122 n = self.ctx.node()
119 123 pbuf = patchbuf()
120 124 cmdutil.export(self.cia.repo, [n], fp=pbuf)
121 125 return patch.diffstat(pbuf.lines) or ''
122 126
123 127 def logmsg(self):
124 128 if self.cia.diffstat:
125 129 diffstat = self.diffstat()
126 130 else:
127 131 diffstat = ''
128 132 self.cia.ui.pushbuffer()
129 133 self.cia.templater.show(self.ctx, changes=self.ctx.changeset(),
130 134 baseurl=self.cia.ui.config('web', 'baseurl'),
131 135 url=self.url, diffstat=diffstat,
132 136 webroot=self.cia.root)
133 137 return self.cia.ui.popbuffer()
134 138
135 139 def xml(self):
136 140 n = short(self.ctx.node())
137 141 src = self.sourceelem(self.cia.project, module=self.cia.module,
138 142 branch=self.ctx.branch())
139 143 # unix timestamp
140 144 dt = self.ctx.date()
141 145 timestamp = dt[0]
142 146
143 147 author = saxutils.escape(self.ctx.user())
144 148 rev = '%d:%s' % (self.ctx.rev(), n)
145 149 log = saxutils.escape(self.logmsg())
146 150
147 151 url = self.url
148 152 if url and url[-1] == '/':
149 153 url = url[:-1]
150 154 url = url and '<url>%s/rev/%s</url>' % (saxutils.escape(url), n) or ''
151 155
152 156 msg = """
153 157 <message>
154 158 <generator>
155 159 <name>Mercurial (hgcia)</name>
156 160 <version>%s</version>
157 161 <url>%s</url>
158 162 <user>%s</user>
159 163 </generator>
160 164 %s
161 165 <body>
162 166 <commit>
163 167 <author>%s</author>
164 168 <version>%s</version>
165 169 <log>%s</log>
166 170 %s
167 171 <files>%s</files>
168 172 </commit>
169 173 </body>
170 174 <timestamp>%d</timestamp>
171 175 </message>
172 176 """ % \
173 177 (HGCIA_VERSION, saxutils.escape(HGCIA_URL),
174 178 saxutils.escape(self.cia.user), src, author, rev, log, url,
175 179 self.fileelems(), timestamp)
176 180
177 181 return msg
178 182
179 183
180 184 class hgcia(object):
181 185 """ CIA notification class """
182 186
183 187 deftemplate = '{desc}'
184 188 dstemplate = '{desc}\n-- \n{diffstat}'
185 189
186 190 def __init__(self, ui, repo):
187 191 self.ui = ui
188 192 self.repo = repo
189 193
190 194 self.ciaurl = self.ui.config('cia', 'url', 'http://cia.vc')
191 195 self.user = self.ui.config('cia', 'user')
192 196 self.project = self.ui.config('cia', 'project')
193 197 self.module = self.ui.config('cia', 'module')
194 198 self.diffstat = self.ui.configbool('cia', 'diffstat')
195 199 self.emailfrom = self.ui.config('email', 'from')
196 200 self.dryrun = self.ui.configbool('cia', 'test')
197 201 self.url = self.ui.config('web', 'baseurl')
198 202 # Default to -1 for backward compatibility
199 203 self.stripcount = int(self.ui.config('cia', 'strip', -1))
200 204 self.root = self.strip(self.repo.root)
201 205
202 206 style = self.ui.config('cia', 'style')
203 207 template = self.ui.config('cia', 'template')
204 208 if not template:
205 209 if self.diffstat:
206 210 template = self.dstemplate
207 211 else:
208 212 template = self.deftemplate
209 213 t = cmdutil.changeset_templater(self.ui, self.repo, False, None,
210 214 template, style, False)
211 215 self.templater = t
212 216
213 217 def strip(self, path):
214 218 '''strip leading slashes from local path, turn into web-safe path.'''
215 219
216 220 path = util.pconvert(path)
217 221 count = self.stripcount
218 222 if count < 0:
219 223 return ''
220 224 while count > 0:
221 225 c = path.find('/')
222 226 if c == -1:
223 227 break
224 228 path = path[c + 1:]
225 229 count -= 1
226 230 return path
227 231
228 232 def sendrpc(self, msg):
229 233 srv = xmlrpclib.Server(self.ciaurl)
230 234 res = srv.hub.deliver(msg)
231 235 if res is not True and res != 'queued.':
232 236 raise util.Abort(_('%s returned an error: %s') %
233 237 (self.ciaurl, res))
234 238
235 239 def sendemail(self, address, data):
236 240 p = email.Parser.Parser()
237 241 msg = p.parsestr(data)
238 242 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
239 243 msg['To'] = address
240 244 msg['From'] = self.emailfrom
241 245 msg['Subject'] = 'DeliverXML'
242 246 msg['Content-type'] = 'text/xml'
243 247 msgtext = msg.as_string()
244 248
245 249 self.ui.status(_('hgcia: sending update to %s\n') % address)
246 250 mail.sendmail(self.ui, util.email(self.emailfrom),
247 251 [address], msgtext)
248 252
249 253
250 254 def hook(ui, repo, hooktype, node=None, url=None, **kwargs):
251 255 """ send CIA notification """
252 256 def sendmsg(cia, ctx):
253 257 msg = ciamsg(cia, ctx).xml()
254 258 if cia.dryrun:
255 259 ui.write(msg)
256 260 elif cia.ciaurl.startswith('mailto:'):
257 261 if not cia.emailfrom:
258 262 raise util.Abort(_('email.from must be defined when '
259 263 'sending by email'))
260 264 cia.sendemail(cia.ciaurl[7:], msg)
261 265 else:
262 266 cia.sendrpc(msg)
263 267
264 268 n = bin(node)
265 269 cia = hgcia(ui, repo)
266 270 if not cia.user:
267 271 ui.debug('cia: no user specified')
268 272 return
269 273 if not cia.project:
270 274 ui.debug('cia: no project specified')
271 275 return
272 276 if hooktype == 'changegroup':
273 277 start = repo.changelog.rev(n)
274 278 end = len(repo.changelog)
275 279 for rev in xrange(start, end):
276 280 n = repo.changelog.node(rev)
277 281 ctx = repo.changectx(n)
278 282 sendmsg(cia, ctx)
279 283 else:
280 284 ctx = repo.changectx(n)
281 285 sendmsg(cia, ctx)
@@ -1,331 +1,335 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 import os
38 38 from mercurial import cmdutil, commands, patch, scmutil, obsolete
39 39 from mercurial.node import nullid, nullrev, short
40 40 from mercurial.i18n import _
41 41
42 42 cmdtable = {}
43 43 command = cmdutil.command(cmdtable)
44 # Note for extension authors: ONLY specify testedwith = 'internal' for
45 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
46 # be specifying the version(s) of Mercurial they are tested with, or
47 # leave the attribute unspecified.
44 48 testedwith = 'internal'
45 49
46 50 @command('debug-diff-tree',
47 51 [('p', 'patch', None, _('generate patch')),
48 52 ('r', 'recursive', None, _('recursive')),
49 53 ('P', 'pretty', None, _('pretty')),
50 54 ('s', 'stdin', None, _('stdin')),
51 55 ('C', 'copy', None, _('detect copies')),
52 56 ('S', 'search', "", _('search'))],
53 57 ('[OPTION]... NODE1 NODE2 [FILE]...'),
54 58 inferrepo=True)
55 59 def difftree(ui, repo, node1=None, node2=None, *files, **opts):
56 60 """diff trees from two commits"""
57 61 def __difftree(repo, node1, node2, files=[]):
58 62 assert node2 is not None
59 63 mmap = repo[node1].manifest()
60 64 mmap2 = repo[node2].manifest()
61 65 m = scmutil.match(repo[node1], files)
62 66 modified, added, removed = repo.status(node1, node2, m)[:3]
63 67 empty = short(nullid)
64 68
65 69 for f in modified:
66 70 # TODO get file permissions
67 71 ui.write(":100664 100664 %s %s M\t%s\t%s\n" %
68 72 (short(mmap[f]), short(mmap2[f]), f, f))
69 73 for f in added:
70 74 ui.write(":000000 100664 %s %s N\t%s\t%s\n" %
71 75 (empty, short(mmap2[f]), f, f))
72 76 for f in removed:
73 77 ui.write(":100664 000000 %s %s D\t%s\t%s\n" %
74 78 (short(mmap[f]), empty, f, f))
75 79 ##
76 80
77 81 while True:
78 82 if opts['stdin']:
79 83 try:
80 84 line = raw_input().split(' ')
81 85 node1 = line[0]
82 86 if len(line) > 1:
83 87 node2 = line[1]
84 88 else:
85 89 node2 = None
86 90 except EOFError:
87 91 break
88 92 node1 = repo.lookup(node1)
89 93 if node2:
90 94 node2 = repo.lookup(node2)
91 95 else:
92 96 node2 = node1
93 97 node1 = repo.changelog.parents(node1)[0]
94 98 if opts['patch']:
95 99 if opts['pretty']:
96 100 catcommit(ui, repo, node2, "")
97 101 m = scmutil.match(repo[node1], files)
98 102 diffopts = patch.difffeatureopts(ui)
99 103 diffopts.git = True
100 104 chunks = patch.diff(repo, node1, node2, match=m,
101 105 opts=diffopts)
102 106 for chunk in chunks:
103 107 ui.write(chunk)
104 108 else:
105 109 __difftree(repo, node1, node2, files=files)
106 110 if not opts['stdin']:
107 111 break
108 112
109 113 def catcommit(ui, repo, n, prefix, ctx=None):
110 114 nlprefix = '\n' + prefix
111 115 if ctx is None:
112 116 ctx = repo[n]
113 117 # use ctx.node() instead ??
114 118 ui.write(("tree %s\n" % short(ctx.changeset()[0])))
115 119 for p in ctx.parents():
116 120 ui.write(("parent %s\n" % p))
117 121
118 122 date = ctx.date()
119 123 description = ctx.description().replace("\0", "")
120 124 ui.write(("author %s %s %s\n" % (ctx.user(), int(date[0]), date[1])))
121 125
122 126 if 'committer' in ctx.extra():
123 127 ui.write(("committer %s\n" % ctx.extra()['committer']))
124 128
125 129 ui.write(("revision %d\n" % ctx.rev()))
126 130 ui.write(("branch %s\n" % ctx.branch()))
127 131 if obsolete.isenabled(repo, obsolete.createmarkersopt):
128 132 if ctx.obsolete():
129 133 ui.write(("obsolete\n"))
130 134 ui.write(("phase %s\n\n" % ctx.phasestr()))
131 135
132 136 if prefix != "":
133 137 ui.write("%s%s\n" % (prefix,
134 138 description.replace('\n', nlprefix).strip()))
135 139 else:
136 140 ui.write(description + "\n")
137 141 if prefix:
138 142 ui.write('\0')
139 143
140 144 @command('debug-merge-base', [], _('REV REV'))
141 145 def base(ui, repo, node1, node2):
142 146 """output common ancestor information"""
143 147 node1 = repo.lookup(node1)
144 148 node2 = repo.lookup(node2)
145 149 n = repo.changelog.ancestor(node1, node2)
146 150 ui.write(short(n) + "\n")
147 151
148 152 @command('debug-cat-file',
149 153 [('s', 'stdin', None, _('stdin'))],
150 154 _('[OPTION]... TYPE FILE'),
151 155 inferrepo=True)
152 156 def catfile(ui, repo, type=None, r=None, **opts):
153 157 """cat a specific revision"""
154 158 # in stdin mode, every line except the commit is prefixed with two
155 159 # spaces. This way the our caller can find the commit without magic
156 160 # strings
157 161 #
158 162 prefix = ""
159 163 if opts['stdin']:
160 164 try:
161 165 (type, r) = raw_input().split(' ')
162 166 prefix = " "
163 167 except EOFError:
164 168 return
165 169
166 170 else:
167 171 if not type or not r:
168 172 ui.warn(_("cat-file: type or revision not supplied\n"))
169 173 commands.help_(ui, 'cat-file')
170 174
171 175 while r:
172 176 if type != "commit":
173 177 ui.warn(_("aborting hg cat-file only understands commits\n"))
174 178 return 1
175 179 n = repo.lookup(r)
176 180 catcommit(ui, repo, n, prefix)
177 181 if opts['stdin']:
178 182 try:
179 183 (type, r) = raw_input().split(' ')
180 184 except EOFError:
181 185 break
182 186 else:
183 187 break
184 188
185 189 # git rev-tree is a confusing thing. You can supply a number of
186 190 # commit sha1s on the command line, and it walks the commit history
187 191 # telling you which commits are reachable from the supplied ones via
188 192 # a bitmask based on arg position.
189 193 # you can specify a commit to stop at by starting the sha1 with ^
190 194 def revtree(ui, args, repo, full="tree", maxnr=0, parents=False):
191 195 def chlogwalk():
192 196 count = len(repo)
193 197 i = count
194 198 l = [0] * 100
195 199 chunk = 100
196 200 while True:
197 201 if chunk > i:
198 202 chunk = i
199 203 i = 0
200 204 else:
201 205 i -= chunk
202 206
203 207 for x in xrange(chunk):
204 208 if i + x >= count:
205 209 l[chunk - x:] = [0] * (chunk - x)
206 210 break
207 211 if full is not None:
208 212 if (i + x) in repo:
209 213 l[x] = repo[i + x]
210 214 l[x].changeset() # force reading
211 215 else:
212 216 if (i + x) in repo:
213 217 l[x] = 1
214 218 for x in xrange(chunk - 1, -1, -1):
215 219 if l[x] != 0:
216 220 yield (i + x, full is not None and l[x] or None)
217 221 if i == 0:
218 222 break
219 223
220 224 # calculate and return the reachability bitmask for sha
221 225 def is_reachable(ar, reachable, sha):
222 226 if len(ar) == 0:
223 227 return 1
224 228 mask = 0
225 229 for i in xrange(len(ar)):
226 230 if sha in reachable[i]:
227 231 mask |= 1 << i
228 232
229 233 return mask
230 234
231 235 reachable = []
232 236 stop_sha1 = []
233 237 want_sha1 = []
234 238 count = 0
235 239
236 240 # figure out which commits they are asking for and which ones they
237 241 # want us to stop on
238 242 for i, arg in enumerate(args):
239 243 if arg.startswith('^'):
240 244 s = repo.lookup(arg[1:])
241 245 stop_sha1.append(s)
242 246 want_sha1.append(s)
243 247 elif arg != 'HEAD':
244 248 want_sha1.append(repo.lookup(arg))
245 249
246 250 # calculate the graph for the supplied commits
247 251 for i, n in enumerate(want_sha1):
248 252 reachable.append(set())
249 253 visit = [n]
250 254 reachable[i].add(n)
251 255 while visit:
252 256 n = visit.pop(0)
253 257 if n in stop_sha1:
254 258 continue
255 259 for p in repo.changelog.parents(n):
256 260 if p not in reachable[i]:
257 261 reachable[i].add(p)
258 262 visit.append(p)
259 263 if p in stop_sha1:
260 264 continue
261 265
262 266 # walk the repository looking for commits that are in our
263 267 # reachability graph
264 268 for i, ctx in chlogwalk():
265 269 if i not in repo:
266 270 continue
267 271 n = repo.changelog.node(i)
268 272 mask = is_reachable(want_sha1, reachable, n)
269 273 if mask:
270 274 parentstr = ""
271 275 if parents:
272 276 pp = repo.changelog.parents(n)
273 277 if pp[0] != nullid:
274 278 parentstr += " " + short(pp[0])
275 279 if pp[1] != nullid:
276 280 parentstr += " " + short(pp[1])
277 281 if not full:
278 282 ui.write("%s%s\n" % (short(n), parentstr))
279 283 elif full == "commit":
280 284 ui.write("%s%s\n" % (short(n), parentstr))
281 285 catcommit(ui, repo, n, ' ', ctx)
282 286 else:
283 287 (p1, p2) = repo.changelog.parents(n)
284 288 (h, h1, h2) = map(short, (n, p1, p2))
285 289 (i1, i2) = map(repo.changelog.rev, (p1, p2))
286 290
287 291 date = ctx.date()[0]
288 292 ui.write("%s %s:%s" % (date, h, mask))
289 293 mask = is_reachable(want_sha1, reachable, p1)
290 294 if i1 != nullrev and mask > 0:
291 295 ui.write("%s:%s " % (h1, mask)),
292 296 mask = is_reachable(want_sha1, reachable, p2)
293 297 if i2 != nullrev and mask > 0:
294 298 ui.write("%s:%s " % (h2, mask))
295 299 ui.write("\n")
296 300 if maxnr and count >= maxnr:
297 301 break
298 302 count += 1
299 303
300 304 # git rev-list tries to order things by date, and has the ability to stop
301 305 # at a given commit without walking the whole repo. TODO add the stop
302 306 # parameter
303 307 @command('debug-rev-list',
304 308 [('H', 'header', None, _('header')),
305 309 ('t', 'topo-order', None, _('topo-order')),
306 310 ('p', 'parents', None, _('parents')),
307 311 ('n', 'max-count', 0, _('max-count'))],
308 312 ('[OPTION]... REV...'))
309 313 def revlist(ui, repo, *revs, **opts):
310 314 """print revisions"""
311 315 if opts['header']:
312 316 full = "commit"
313 317 else:
314 318 full = None
315 319 copy = [x for x in revs]
316 320 revtree(ui, copy, repo, full, opts['max_count'], opts['parents'])
317 321
318 322 @command('view',
319 323 [('l', 'limit', '',
320 324 _('limit number of changes displayed'), _('NUM'))],
321 325 _('[-l LIMIT] [REVRANGE]'))
322 326 def view(ui, repo, *etc, **opts):
323 327 "start interactive history viewer"
324 328 os.chdir(repo.root)
325 329 optstr = ' '.join(['--%s %s' % (k, v) for k, v in opts.iteritems() if v])
326 330 if repo.filtername is None:
327 331 optstr += '--hidden'
328 332
329 333 cmd = ui.config("hgk", "path", "hgk") + " %s %s" % (optstr, " ".join(etc))
330 334 ui.debug("running %s\n" % cmd)
331 335 ui.system(cmd)
@@ -1,64 +1,68 b''
1 1 # highlight - syntax highlighting in hgweb, based on Pygments
2 2 #
3 3 # Copyright 2008, 2009 Patrick Mezard <pmezard@gmail.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 # The original module was split in an interface and an implementation
9 9 # file to defer pygments loading and speedup extension setup.
10 10
11 11 """syntax highlighting for hgweb (requires Pygments)
12 12
13 13 It depends on the Pygments syntax highlighting library:
14 14 http://pygments.org/
15 15
16 16 There is a single configuration option::
17 17
18 18 [web]
19 19 pygments_style = <style>
20 20
21 21 The default is 'colorful'.
22 22 """
23 23
24 24 import highlight
25 25 from mercurial.hgweb import webcommands, webutil, common
26 26 from mercurial import extensions, encoding
27 # Note for extension authors: ONLY specify testedwith = 'internal' for
28 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
29 # be specifying the version(s) of Mercurial they are tested with, or
30 # leave the attribute unspecified.
27 31 testedwith = 'internal'
28 32
29 33 def filerevision_highlight(orig, web, tmpl, fctx):
30 34 mt = ''.join(tmpl('mimetype', encoding=encoding.encoding))
31 35 # only pygmentize for mimetype containing 'html' so we both match
32 36 # 'text/html' and possibly 'application/xhtml+xml' in the future
33 37 # so that we don't have to touch the extension when the mimetype
34 38 # for a template changes; also hgweb optimizes the case that a
35 39 # raw file is sent using rawfile() and doesn't call us, so we
36 40 # can't clash with the file's content-type here in case we
37 41 # pygmentize a html file
38 42 if 'html' in mt:
39 43 style = web.config('web', 'pygments_style', 'colorful')
40 44 highlight.pygmentize('fileline', fctx, style, tmpl)
41 45 return orig(web, tmpl, fctx)
42 46
43 47 def annotate_highlight(orig, web, req, tmpl):
44 48 mt = ''.join(tmpl('mimetype', encoding=encoding.encoding))
45 49 if 'html' in mt:
46 50 fctx = webutil.filectx(web.repo, req)
47 51 style = web.config('web', 'pygments_style', 'colorful')
48 52 highlight.pygmentize('annotateline', fctx, style, tmpl)
49 53 return orig(web, req, tmpl)
50 54
51 55 def generate_css(web, req, tmpl):
52 56 pg_style = web.config('web', 'pygments_style', 'colorful')
53 57 fmter = highlight.HtmlFormatter(style=pg_style)
54 58 req.respond(common.HTTP_OK, 'text/css')
55 59 return ['/* pygments_style = %s */\n\n' % pg_style,
56 60 fmter.get_style_defs('')]
57 61
58 62 def extsetup():
59 63 # monkeypatch in the new version
60 64 extensions.wrapfunction(webcommands, '_filerevision',
61 65 filerevision_highlight)
62 66 extensions.wrapfunction(webcommands, 'annotate', annotate_highlight)
63 67 webcommands.highlightcss = generate_css
64 68 webcommands.__all__.append('highlightcss')
@@ -1,1151 +1,1155 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
40 40 # d, drop = remove commit from history
41 41 # m, mess = edit message without changing commit content
42 42 #
43 43
44 44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 45 for each revision in your history. For example, if you had meant to add gamma
46 46 before beta, and then wanted to add delta in the same revision as beta, you
47 47 would reorganize the file to look like this::
48 48
49 49 pick 030b686bedc4 Add gamma
50 50 pick c561b4e977df Add beta
51 51 fold 7c2fd3b9020c Add delta
52 52
53 53 # Edit history between c561b4e977df and 7c2fd3b9020c
54 54 #
55 55 # Commits are listed from least to most recent
56 56 #
57 57 # Commands:
58 58 # p, pick = use commit
59 59 # e, edit = use commit, but stop for amending
60 60 # f, fold = use commit, but combine it with the one above
61 61 # r, roll = like fold, but discard this commit's description
62 62 # d, drop = remove commit from history
63 63 # m, mess = edit message without changing commit content
64 64 #
65 65
66 66 At which point you close the editor and ``histedit`` starts working. When you
67 67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
68 68 those revisions together, offering you a chance to clean up the commit message::
69 69
70 70 Add beta
71 71 ***
72 72 Add delta
73 73
74 74 Edit the commit message to your liking, then close the editor. For
75 75 this example, let's assume that the commit message was changed to
76 76 ``Add beta and delta.`` After histedit has run and had a chance to
77 77 remove any old or temporary revisions it needed, the history looks
78 78 like this::
79 79
80 80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
81 81 | Add beta and delta.
82 82 |
83 83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
84 84 | Add gamma
85 85 |
86 86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
87 87 Add alpha
88 88
89 89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
90 90 ones) until after it has completed all the editing operations, so it will
91 91 probably perform several strip operations when it's done. For the above example,
92 92 it had to run strip twice. Strip can be slow depending on a variety of factors,
93 93 so you might need to be a little patient. You can choose to keep the original
94 94 revisions by passing the ``--keep`` flag.
95 95
96 96 The ``edit`` operation will drop you back to a command prompt,
97 97 allowing you to edit files freely, or even use ``hg record`` to commit
98 98 some changes as a separate commit. When you're done, any remaining
99 99 uncommitted changes will be committed as well. When done, run ``hg
100 100 histedit --continue`` to finish this step. You'll be prompted for a
101 101 new commit message, but the default commit message will be the
102 102 original message for the ``edit`` ed revision.
103 103
104 104 The ``message`` operation will give you a chance to revise a commit
105 105 message without changing the contents. It's a shortcut for doing
106 106 ``edit`` immediately followed by `hg histedit --continue``.
107 107
108 108 If ``histedit`` encounters a conflict when moving a revision (while
109 109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
110 110 ``edit`` with the difference that it won't prompt you for a commit
111 111 message when done. If you decide at this point that you don't like how
112 112 much work it will be to rearrange history, or that you made a mistake,
113 113 you can use ``hg histedit --abort`` to abandon the new changes you
114 114 have made and return to the state before you attempted to edit your
115 115 history.
116 116
117 117 If we clone the histedit-ed example repository above and add four more
118 118 changes, such that we have the following history::
119 119
120 120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
121 121 | Add theta
122 122 |
123 123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
124 124 | Add eta
125 125 |
126 126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
127 127 | Add zeta
128 128 |
129 129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
130 130 | Add epsilon
131 131 |
132 132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
133 133 | Add beta and delta.
134 134 |
135 135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
136 136 | Add gamma
137 137 |
138 138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
139 139 Add alpha
140 140
141 141 If you run ``hg histedit --outgoing`` on the clone then it is the same
142 142 as running ``hg histedit 836302820282``. If you need plan to push to a
143 143 repository that Mercurial does not detect to be related to the source
144 144 repo, you can add a ``--force`` option.
145 145
146 146 Histedit rule lines are truncated to 80 characters by default. You
147 147 can customise this behaviour by setting a different length in your
148 148 configuration file::
149 149
150 150 [histedit]
151 151 linelen = 120 # truncate rule lines at 120 characters
152 152 """
153 153
154 154 try:
155 155 import cPickle as pickle
156 156 pickle.dump # import now
157 157 except ImportError:
158 158 import pickle
159 159 import errno
160 160 import os
161 161 import sys
162 162
163 163 from mercurial import cmdutil
164 164 from mercurial import discovery
165 165 from mercurial import error
166 166 from mercurial import changegroup
167 167 from mercurial import copies
168 168 from mercurial import context
169 169 from mercurial import exchange
170 170 from mercurial import extensions
171 171 from mercurial import hg
172 172 from mercurial import node
173 173 from mercurial import repair
174 174 from mercurial import scmutil
175 175 from mercurial import util
176 176 from mercurial import obsolete
177 177 from mercurial import merge as mergemod
178 178 from mercurial.lock import release
179 179 from mercurial.i18n import _
180 180
181 181 cmdtable = {}
182 182 command = cmdutil.command(cmdtable)
183 183
184 # Note for extension authors: ONLY specify testedwith = 'internal' for
185 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
186 # be specifying the version(s) of Mercurial they are tested with, or
187 # leave the attribute unspecified.
184 188 testedwith = 'internal'
185 189
186 190 # i18n: command names and abbreviations must remain untranslated
187 191 editcomment = _("""# Edit history between %s and %s
188 192 #
189 193 # Commits are listed from least to most recent
190 194 #
191 195 # Commands:
192 196 # p, pick = use commit
193 197 # e, edit = use commit, but stop for amending
194 198 # f, fold = use commit, but combine it with the one above
195 199 # r, roll = like fold, but discard this commit's description
196 200 # d, drop = remove commit from history
197 201 # m, mess = edit message without changing commit content
198 202 #
199 203 """)
200 204
201 205 class histeditstate(object):
202 206 def __init__(self, repo, parentctxnode=None, rules=None, keep=None,
203 207 topmost=None, replacements=None, lock=None, wlock=None):
204 208 self.repo = repo
205 209 self.rules = rules
206 210 self.keep = keep
207 211 self.topmost = topmost
208 212 self.parentctxnode = parentctxnode
209 213 self.lock = lock
210 214 self.wlock = wlock
211 215 self.backupfile = None
212 216 if replacements is None:
213 217 self.replacements = []
214 218 else:
215 219 self.replacements = replacements
216 220
217 221 def read(self):
218 222 """Load histedit state from disk and set fields appropriately."""
219 223 try:
220 224 fp = self.repo.vfs('histedit-state', 'r')
221 225 except IOError, err:
222 226 if err.errno != errno.ENOENT:
223 227 raise
224 228 raise util.Abort(_('no histedit in progress'))
225 229
226 230 try:
227 231 data = pickle.load(fp)
228 232 parentctxnode, rules, keep, topmost, replacements = data
229 233 backupfile = None
230 234 except pickle.UnpicklingError:
231 235 data = self._load()
232 236 parentctxnode, rules, keep, topmost, replacements, backupfile = data
233 237
234 238 self.parentctxnode = parentctxnode
235 239 self.rules = rules
236 240 self.keep = keep
237 241 self.topmost = topmost
238 242 self.replacements = replacements
239 243 self.backupfile = backupfile
240 244
241 245 def write(self):
242 246 fp = self.repo.vfs('histedit-state', 'w')
243 247 fp.write('v1\n')
244 248 fp.write('%s\n' % node.hex(self.parentctxnode))
245 249 fp.write('%s\n' % node.hex(self.topmost))
246 250 fp.write('%s\n' % self.keep)
247 251 fp.write('%d\n' % len(self.rules))
248 252 for rule in self.rules:
249 253 fp.write('%s\n' % rule[0]) # action
250 254 fp.write('%s\n' % rule[1]) # remainder
251 255 fp.write('%d\n' % len(self.replacements))
252 256 for replacement in self.replacements:
253 257 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
254 258 for r in replacement[1])))
255 259 backupfile = self.backupfile
256 260 if not backupfile:
257 261 backupfile = ''
258 262 fp.write('%s\n' % backupfile)
259 263 fp.close()
260 264
261 265 def _load(self):
262 266 fp = self.repo.vfs('histedit-state', 'r')
263 267 lines = [l[:-1] for l in fp.readlines()]
264 268
265 269 index = 0
266 270 lines[index] # version number
267 271 index += 1
268 272
269 273 parentctxnode = node.bin(lines[index])
270 274 index += 1
271 275
272 276 topmost = node.bin(lines[index])
273 277 index += 1
274 278
275 279 keep = lines[index] == 'True'
276 280 index += 1
277 281
278 282 # Rules
279 283 rules = []
280 284 rulelen = int(lines[index])
281 285 index += 1
282 286 for i in xrange(rulelen):
283 287 ruleaction = lines[index]
284 288 index += 1
285 289 rule = lines[index]
286 290 index += 1
287 291 rules.append((ruleaction, rule))
288 292
289 293 # Replacements
290 294 replacements = []
291 295 replacementlen = int(lines[index])
292 296 index += 1
293 297 for i in xrange(replacementlen):
294 298 replacement = lines[index]
295 299 original = node.bin(replacement[:40])
296 300 succ = [node.bin(replacement[i:i + 40]) for i in
297 301 range(40, len(replacement), 40)]
298 302 replacements.append((original, succ))
299 303 index += 1
300 304
301 305 backupfile = lines[index]
302 306 index += 1
303 307
304 308 fp.close()
305 309
306 310 return parentctxnode, rules, keep, topmost, replacements, backupfile
307 311
308 312 def clear(self):
309 313 self.repo.vfs.unlink('histedit-state')
310 314
311 315 class histeditaction(object):
312 316 def __init__(self, state, node):
313 317 self.state = state
314 318 self.repo = state.repo
315 319 self.node = node
316 320
317 321 @classmethod
318 322 def fromrule(cls, state, rule):
319 323 """Parses the given rule, returning an instance of the histeditaction.
320 324 """
321 325 repo = state.repo
322 326 rulehash = rule.strip().split(' ', 1)[0]
323 327 try:
324 328 node = repo[rulehash].node()
325 329 except error.RepoError:
326 330 raise util.Abort(_('unknown changeset %s listed') % rulehash[:12])
327 331 return cls(state, node)
328 332
329 333 def run(self):
330 334 """Runs the action. The default behavior is simply apply the action's
331 335 rulectx onto the current parentctx."""
332 336 self.applychange()
333 337 self.continuedirty()
334 338 return self.continueclean()
335 339
336 340 def applychange(self):
337 341 """Applies the changes from this action's rulectx onto the current
338 342 parentctx, but does not commit them."""
339 343 repo = self.repo
340 344 rulectx = repo[self.node]
341 345 hg.update(repo, self.state.parentctxnode)
342 346 stats = applychanges(repo.ui, repo, rulectx, {})
343 347 if stats and stats[3] > 0:
344 348 raise error.InterventionRequired(_('Fix up the change and run '
345 349 'hg histedit --continue'))
346 350
347 351 def continuedirty(self):
348 352 """Continues the action when changes have been applied to the working
349 353 copy. The default behavior is to commit the dirty changes."""
350 354 repo = self.repo
351 355 rulectx = repo[self.node]
352 356
353 357 editor = self.commiteditor()
354 358 commit = commitfuncfor(repo, rulectx)
355 359
356 360 commit(text=rulectx.description(), user=rulectx.user(),
357 361 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
358 362
359 363 def commiteditor(self):
360 364 """The editor to be used to edit the commit message."""
361 365 return False
362 366
363 367 def continueclean(self):
364 368 """Continues the action when the working copy is clean. The default
365 369 behavior is to accept the current commit as the new version of the
366 370 rulectx."""
367 371 ctx = self.repo['.']
368 372 if ctx.node() == self.state.parentctxnode:
369 373 self.repo.ui.warn(_('%s: empty changeset\n') %
370 374 node.short(self.node))
371 375 return ctx, [(self.node, tuple())]
372 376 if ctx.node() == self.node:
373 377 # Nothing changed
374 378 return ctx, []
375 379 return ctx, [(self.node, (ctx.node(),))]
376 380
377 381 def commitfuncfor(repo, src):
378 382 """Build a commit function for the replacement of <src>
379 383
380 384 This function ensure we apply the same treatment to all changesets.
381 385
382 386 - Add a 'histedit_source' entry in extra.
383 387
384 388 Note that fold have its own separated logic because its handling is a bit
385 389 different and not easily factored out of the fold method.
386 390 """
387 391 phasemin = src.phase()
388 392 def commitfunc(**kwargs):
389 393 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
390 394 try:
391 395 repo.ui.setconfig('phases', 'new-commit', phasemin,
392 396 'histedit')
393 397 extra = kwargs.get('extra', {}).copy()
394 398 extra['histedit_source'] = src.hex()
395 399 kwargs['extra'] = extra
396 400 return repo.commit(**kwargs)
397 401 finally:
398 402 repo.ui.restoreconfig(phasebackup)
399 403 return commitfunc
400 404
401 405 def applychanges(ui, repo, ctx, opts):
402 406 """Merge changeset from ctx (only) in the current working directory"""
403 407 wcpar = repo.dirstate.parents()[0]
404 408 if ctx.p1().node() == wcpar:
405 409 # edition ar "in place" we do not need to make any merge,
406 410 # just applies changes on parent for edition
407 411 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
408 412 stats = None
409 413 else:
410 414 try:
411 415 # ui.forcemerge is an internal variable, do not document
412 416 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
413 417 'histedit')
414 418 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
415 419 finally:
416 420 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
417 421 return stats
418 422
419 423 def collapse(repo, first, last, commitopts, skipprompt=False):
420 424 """collapse the set of revisions from first to last as new one.
421 425
422 426 Expected commit options are:
423 427 - message
424 428 - date
425 429 - username
426 430 Commit message is edited in all cases.
427 431
428 432 This function works in memory."""
429 433 ctxs = list(repo.set('%d::%d', first, last))
430 434 if not ctxs:
431 435 return None
432 436 base = first.parents()[0]
433 437
434 438 # commit a new version of the old changeset, including the update
435 439 # collect all files which might be affected
436 440 files = set()
437 441 for ctx in ctxs:
438 442 files.update(ctx.files())
439 443
440 444 # Recompute copies (avoid recording a -> b -> a)
441 445 copied = copies.pathcopies(base, last)
442 446
443 447 # prune files which were reverted by the updates
444 448 def samefile(f):
445 449 if f in last.manifest():
446 450 a = last.filectx(f)
447 451 if f in base.manifest():
448 452 b = base.filectx(f)
449 453 return (a.data() == b.data()
450 454 and a.flags() == b.flags())
451 455 else:
452 456 return False
453 457 else:
454 458 return f not in base.manifest()
455 459 files = [f for f in files if not samefile(f)]
456 460 # commit version of these files as defined by head
457 461 headmf = last.manifest()
458 462 def filectxfn(repo, ctx, path):
459 463 if path in headmf:
460 464 fctx = last[path]
461 465 flags = fctx.flags()
462 466 mctx = context.memfilectx(repo,
463 467 fctx.path(), fctx.data(),
464 468 islink='l' in flags,
465 469 isexec='x' in flags,
466 470 copied=copied.get(path))
467 471 return mctx
468 472 return None
469 473
470 474 if commitopts.get('message'):
471 475 message = commitopts['message']
472 476 else:
473 477 message = first.description()
474 478 user = commitopts.get('user')
475 479 date = commitopts.get('date')
476 480 extra = commitopts.get('extra')
477 481
478 482 parents = (first.p1().node(), first.p2().node())
479 483 editor = None
480 484 if not skipprompt:
481 485 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
482 486 new = context.memctx(repo,
483 487 parents=parents,
484 488 text=message,
485 489 files=files,
486 490 filectxfn=filectxfn,
487 491 user=user,
488 492 date=date,
489 493 extra=extra,
490 494 editor=editor)
491 495 return repo.commitctx(new)
492 496
493 497 class pick(histeditaction):
494 498 def run(self):
495 499 rulectx = self.repo[self.node]
496 500 if rulectx.parents()[0].node() == self.state.parentctxnode:
497 501 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
498 502 return rulectx, []
499 503
500 504 return super(pick, self).run()
501 505
502 506 class edit(histeditaction):
503 507 def run(self):
504 508 repo = self.repo
505 509 rulectx = repo[self.node]
506 510 hg.update(repo, self.state.parentctxnode)
507 511 applychanges(repo.ui, repo, rulectx, {})
508 512 raise error.InterventionRequired(
509 513 _('Make changes as needed, you may commit or record as needed '
510 514 'now.\nWhen you are finished, run hg histedit --continue to '
511 515 'resume.'))
512 516
513 517 def commiteditor(self):
514 518 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
515 519
516 520 class fold(histeditaction):
517 521 def continuedirty(self):
518 522 repo = self.repo
519 523 rulectx = repo[self.node]
520 524
521 525 commit = commitfuncfor(repo, rulectx)
522 526 commit(text='fold-temp-revision %s' % node.short(self.node),
523 527 user=rulectx.user(), date=rulectx.date(),
524 528 extra=rulectx.extra())
525 529
526 530 def continueclean(self):
527 531 repo = self.repo
528 532 ctx = repo['.']
529 533 rulectx = repo[self.node]
530 534 parentctxnode = self.state.parentctxnode
531 535 if ctx.node() == parentctxnode:
532 536 repo.ui.warn(_('%s: empty changeset\n') %
533 537 node.short(self.node))
534 538 return ctx, [(self.node, (parentctxnode,))]
535 539
536 540 parentctx = repo[parentctxnode]
537 541 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
538 542 parentctx))
539 543 if not newcommits:
540 544 repo.ui.warn(_('%s: cannot fold - working copy is not a '
541 545 'descendant of previous commit %s\n') %
542 546 (node.short(self.node), node.short(parentctxnode)))
543 547 return ctx, [(self.node, (ctx.node(),))]
544 548
545 549 middlecommits = newcommits.copy()
546 550 middlecommits.discard(ctx.node())
547 551
548 552 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
549 553 middlecommits)
550 554
551 555 def skipprompt(self):
552 556 return False
553 557
554 558 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
555 559 parent = ctx.parents()[0].node()
556 560 hg.update(repo, parent)
557 561 ### prepare new commit data
558 562 commitopts = {}
559 563 commitopts['user'] = ctx.user()
560 564 # commit message
561 565 if self.skipprompt():
562 566 newmessage = ctx.description()
563 567 else:
564 568 newmessage = '\n***\n'.join(
565 569 [ctx.description()] +
566 570 [repo[r].description() for r in internalchanges] +
567 571 [oldctx.description()]) + '\n'
568 572 commitopts['message'] = newmessage
569 573 # date
570 574 commitopts['date'] = max(ctx.date(), oldctx.date())
571 575 extra = ctx.extra().copy()
572 576 # histedit_source
573 577 # note: ctx is likely a temporary commit but that the best we can do
574 578 # here. This is sufficient to solve issue3681 anyway.
575 579 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
576 580 commitopts['extra'] = extra
577 581 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
578 582 try:
579 583 phasemin = max(ctx.phase(), oldctx.phase())
580 584 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
581 585 n = collapse(repo, ctx, repo[newnode], commitopts,
582 586 skipprompt=self.skipprompt())
583 587 finally:
584 588 repo.ui.restoreconfig(phasebackup)
585 589 if n is None:
586 590 return ctx, []
587 591 hg.update(repo, n)
588 592 replacements = [(oldctx.node(), (newnode,)),
589 593 (ctx.node(), (n,)),
590 594 (newnode, (n,)),
591 595 ]
592 596 for ich in internalchanges:
593 597 replacements.append((ich, (n,)))
594 598 return repo[n], replacements
595 599
596 600 class rollup(fold):
597 601 def skipprompt(self):
598 602 return True
599 603
600 604 class drop(histeditaction):
601 605 def run(self):
602 606 parentctx = self.repo[self.state.parentctxnode]
603 607 return parentctx, [(self.node, tuple())]
604 608
605 609 class message(histeditaction):
606 610 def commiteditor(self):
607 611 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
608 612
609 613 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
610 614 """utility function to find the first outgoing changeset
611 615
612 616 Used by initialisation code"""
613 617 dest = ui.expandpath(remote or 'default-push', remote or 'default')
614 618 dest, revs = hg.parseurl(dest, None)[:2]
615 619 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
616 620
617 621 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
618 622 other = hg.peer(repo, opts, dest)
619 623
620 624 if revs:
621 625 revs = [repo.lookup(rev) for rev in revs]
622 626
623 627 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
624 628 if not outgoing.missing:
625 629 raise util.Abort(_('no outgoing ancestors'))
626 630 roots = list(repo.revs("roots(%ln)", outgoing.missing))
627 631 if 1 < len(roots):
628 632 msg = _('there are ambiguous outgoing revisions')
629 633 hint = _('see "hg help histedit" for more detail')
630 634 raise util.Abort(msg, hint=hint)
631 635 return repo.lookup(roots[0])
632 636
633 637 actiontable = {'p': pick,
634 638 'pick': pick,
635 639 'e': edit,
636 640 'edit': edit,
637 641 'f': fold,
638 642 'fold': fold,
639 643 'r': rollup,
640 644 'roll': rollup,
641 645 'd': drop,
642 646 'drop': drop,
643 647 'm': message,
644 648 'mess': message,
645 649 }
646 650
647 651 @command('histedit',
648 652 [('', 'commands', '',
649 653 _('read history edits from the specified file'), _('FILE')),
650 654 ('c', 'continue', False, _('continue an edit already in progress')),
651 655 ('', 'edit-plan', False, _('edit remaining actions list')),
652 656 ('k', 'keep', False,
653 657 _("don't strip old nodes after edit is complete")),
654 658 ('', 'abort', False, _('abort an edit in progress')),
655 659 ('o', 'outgoing', False, _('changesets not found in destination')),
656 660 ('f', 'force', False,
657 661 _('force outgoing even for unrelated repositories')),
658 662 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
659 663 _("ANCESTOR | --outgoing [URL]"))
660 664 def histedit(ui, repo, *freeargs, **opts):
661 665 """interactively edit changeset history
662 666
663 667 This command edits changesets between ANCESTOR and the parent of
664 668 the working directory.
665 669
666 670 With --outgoing, this edits changesets not found in the
667 671 destination repository. If URL of the destination is omitted, the
668 672 'default-push' (or 'default') path will be used.
669 673
670 674 For safety, this command is aborted, also if there are ambiguous
671 675 outgoing revisions which may confuse users: for example, there are
672 676 multiple branches containing outgoing revisions.
673 677
674 678 Use "min(outgoing() and ::.)" or similar revset specification
675 679 instead of --outgoing to specify edit target revision exactly in
676 680 such ambiguous situation. See :hg:`help revsets` for detail about
677 681 selecting revisions.
678 682
679 683 Returns 0 on success, 1 if user intervention is required (not only
680 684 for intentional "edit" command, but also for resolving unexpected
681 685 conflicts).
682 686 """
683 687 state = histeditstate(repo)
684 688 try:
685 689 state.wlock = repo.wlock()
686 690 state.lock = repo.lock()
687 691 _histedit(ui, repo, state, *freeargs, **opts)
688 692 finally:
689 693 release(state.lock, state.wlock)
690 694
691 695 def _histedit(ui, repo, state, *freeargs, **opts):
692 696 # TODO only abort if we try and histedit mq patches, not just
693 697 # blanket if mq patches are applied somewhere
694 698 mq = getattr(repo, 'mq', None)
695 699 if mq and mq.applied:
696 700 raise util.Abort(_('source has mq patches applied'))
697 701
698 702 # basic argument incompatibility processing
699 703 outg = opts.get('outgoing')
700 704 cont = opts.get('continue')
701 705 editplan = opts.get('edit_plan')
702 706 abort = opts.get('abort')
703 707 force = opts.get('force')
704 708 rules = opts.get('commands', '')
705 709 revs = opts.get('rev', [])
706 710 goal = 'new' # This invocation goal, in new, continue, abort
707 711 if force and not outg:
708 712 raise util.Abort(_('--force only allowed with --outgoing'))
709 713 if cont:
710 714 if any((outg, abort, revs, freeargs, rules, editplan)):
711 715 raise util.Abort(_('no arguments allowed with --continue'))
712 716 goal = 'continue'
713 717 elif abort:
714 718 if any((outg, revs, freeargs, rules, editplan)):
715 719 raise util.Abort(_('no arguments allowed with --abort'))
716 720 goal = 'abort'
717 721 elif editplan:
718 722 if any((outg, revs, freeargs)):
719 723 raise util.Abort(_('only --commands argument allowed with '
720 724 '--edit-plan'))
721 725 goal = 'edit-plan'
722 726 else:
723 727 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
724 728 raise util.Abort(_('history edit already in progress, try '
725 729 '--continue or --abort'))
726 730 if outg:
727 731 if revs:
728 732 raise util.Abort(_('no revisions allowed with --outgoing'))
729 733 if len(freeargs) > 1:
730 734 raise util.Abort(
731 735 _('only one repo argument allowed with --outgoing'))
732 736 else:
733 737 revs.extend(freeargs)
734 738 if len(revs) == 0:
735 739 histeditdefault = ui.config('histedit', 'defaultrev')
736 740 if histeditdefault:
737 741 revs.append(histeditdefault)
738 742 if len(revs) != 1:
739 743 raise util.Abort(
740 744 _('histedit requires exactly one ancestor revision'))
741 745
742 746
743 747 replacements = []
744 748 keep = opts.get('keep', False)
745 749
746 750 # rebuild state
747 751 if goal == 'continue':
748 752 state.read()
749 753 state = bootstrapcontinue(ui, state, opts)
750 754 elif goal == 'edit-plan':
751 755 state.read()
752 756 if not rules:
753 757 comment = editcomment % (node.short(state.parentctxnode),
754 758 node.short(state.topmost))
755 759 rules = ruleeditor(repo, ui, state.rules, comment)
756 760 else:
757 761 if rules == '-':
758 762 f = sys.stdin
759 763 else:
760 764 f = open(rules)
761 765 rules = f.read()
762 766 f.close()
763 767 rules = [l for l in (r.strip() for r in rules.splitlines())
764 768 if l and not l.startswith('#')]
765 769 rules = verifyrules(rules, repo, [repo[c] for [_a, c] in state.rules])
766 770 state.rules = rules
767 771 state.write()
768 772 return
769 773 elif goal == 'abort':
770 774 state.read()
771 775 mapping, tmpnodes, leafs, _ntm = processreplacement(state)
772 776 ui.debug('restore wc to old parent %s\n' % node.short(state.topmost))
773 777
774 778 # Recover our old commits if necessary
775 779 if not state.topmost in repo and state.backupfile:
776 780 backupfile = repo.join(state.backupfile)
777 781 f = hg.openpath(ui, backupfile)
778 782 gen = exchange.readbundle(ui, f, backupfile)
779 783 changegroup.addchangegroup(repo, gen, 'histedit',
780 784 'bundle:' + backupfile)
781 785 os.remove(backupfile)
782 786
783 787 # check whether we should update away
784 788 parentnodes = [c.node() for c in repo[None].parents()]
785 789 for n in leafs | set([state.parentctxnode]):
786 790 if n in parentnodes:
787 791 hg.clean(repo, state.topmost)
788 792 break
789 793 else:
790 794 pass
791 795 cleanupnode(ui, repo, 'created', tmpnodes)
792 796 cleanupnode(ui, repo, 'temp', leafs)
793 797 state.clear()
794 798 return
795 799 else:
796 800 cmdutil.checkunfinished(repo)
797 801 cmdutil.bailifchanged(repo)
798 802
799 803 topmost, empty = repo.dirstate.parents()
800 804 if outg:
801 805 if freeargs:
802 806 remote = freeargs[0]
803 807 else:
804 808 remote = None
805 809 root = findoutgoing(ui, repo, remote, force, opts)
806 810 else:
807 811 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
808 812 if len(rr) != 1:
809 813 raise util.Abort(_('The specified revisions must have '
810 814 'exactly one common root'))
811 815 root = rr[0].node()
812 816
813 817 revs = between(repo, root, topmost, keep)
814 818 if not revs:
815 819 raise util.Abort(_('%s is not an ancestor of working directory') %
816 820 node.short(root))
817 821
818 822 ctxs = [repo[r] for r in revs]
819 823 if not rules:
820 824 comment = editcomment % (node.short(root), node.short(topmost))
821 825 rules = ruleeditor(repo, ui, [['pick', c] for c in ctxs], comment)
822 826 else:
823 827 if rules == '-':
824 828 f = sys.stdin
825 829 else:
826 830 f = open(rules)
827 831 rules = f.read()
828 832 f.close()
829 833 rules = [l for l in (r.strip() for r in rules.splitlines())
830 834 if l and not l.startswith('#')]
831 835 rules = verifyrules(rules, repo, ctxs)
832 836
833 837 parentctxnode = repo[root].parents()[0].node()
834 838
835 839 state.parentctxnode = parentctxnode
836 840 state.rules = rules
837 841 state.keep = keep
838 842 state.topmost = topmost
839 843 state.replacements = replacements
840 844
841 845 # Create a backup so we can always abort completely.
842 846 backupfile = None
843 847 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
844 848 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
845 849 'histedit')
846 850 state.backupfile = backupfile
847 851
848 852 while state.rules:
849 853 state.write()
850 854 action, ha = state.rules.pop(0)
851 855 ui.debug('histedit: processing %s %s\n' % (action, ha[:12]))
852 856 actobj = actiontable[action].fromrule(state, ha)
853 857 parentctx, replacement_ = actobj.run()
854 858 state.parentctxnode = parentctx.node()
855 859 state.replacements.extend(replacement_)
856 860 state.write()
857 861
858 862 hg.update(repo, state.parentctxnode)
859 863
860 864 mapping, tmpnodes, created, ntm = processreplacement(state)
861 865 if mapping:
862 866 for prec, succs in mapping.iteritems():
863 867 if not succs:
864 868 ui.debug('histedit: %s is dropped\n' % node.short(prec))
865 869 else:
866 870 ui.debug('histedit: %s is replaced by %s\n' % (
867 871 node.short(prec), node.short(succs[0])))
868 872 if len(succs) > 1:
869 873 m = 'histedit: %s'
870 874 for n in succs[1:]:
871 875 ui.debug(m % node.short(n))
872 876
873 877 if not keep:
874 878 if mapping:
875 879 movebookmarks(ui, repo, mapping, state.topmost, ntm)
876 880 # TODO update mq state
877 881 if obsolete.isenabled(repo, obsolete.createmarkersopt):
878 882 markers = []
879 883 # sort by revision number because it sound "right"
880 884 for prec in sorted(mapping, key=repo.changelog.rev):
881 885 succs = mapping[prec]
882 886 markers.append((repo[prec],
883 887 tuple(repo[s] for s in succs)))
884 888 if markers:
885 889 obsolete.createmarkers(repo, markers)
886 890 else:
887 891 cleanupnode(ui, repo, 'replaced', mapping)
888 892
889 893 cleanupnode(ui, repo, 'temp', tmpnodes)
890 894 state.clear()
891 895 if os.path.exists(repo.sjoin('undo')):
892 896 os.unlink(repo.sjoin('undo'))
893 897
894 898 def bootstrapcontinue(ui, state, opts):
895 899 repo = state.repo
896 900 if state.rules:
897 901 action, currentnode = state.rules.pop(0)
898 902
899 903 actobj = actiontable[action].fromrule(state, currentnode)
900 904
901 905 s = repo.status()
902 906 if s.modified or s.added or s.removed or s.deleted:
903 907 actobj.continuedirty()
904 908 s = repo.status()
905 909 if s.modified or s.added or s.removed or s.deleted:
906 910 raise util.Abort(_("working copy still dirty"))
907 911
908 912 parentctx, replacements = actobj.continueclean()
909 913
910 914 state.parentctxnode = parentctx.node()
911 915 state.replacements.extend(replacements)
912 916
913 917 return state
914 918
915 919 def between(repo, old, new, keep):
916 920 """select and validate the set of revision to edit
917 921
918 922 When keep is false, the specified set can't have children."""
919 923 ctxs = list(repo.set('%n::%n', old, new))
920 924 if ctxs and not keep:
921 925 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
922 926 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
923 927 raise util.Abort(_('cannot edit history that would orphan nodes'))
924 928 if repo.revs('(%ld) and merge()', ctxs):
925 929 raise util.Abort(_('cannot edit history that contains merges'))
926 930 root = ctxs[0] # list is already sorted by repo.set
927 931 if not root.mutable():
928 932 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
929 933 return [c.node() for c in ctxs]
930 934
931 935 def makedesc(repo, action, rev):
932 936 """build a initial action line for a ctx
933 937
934 938 line are in the form:
935 939
936 940 <action> <hash> <rev> <summary>
937 941 """
938 942 ctx = repo[rev]
939 943 summary = ''
940 944 if ctx.description():
941 945 summary = ctx.description().splitlines()[0]
942 946 line = '%s %s %d %s' % (action, ctx, ctx.rev(), summary)
943 947 # trim to 80 columns so it's not stupidly wide in my editor
944 948 maxlen = repo.ui.configint('histedit', 'linelen', default=80)
945 949 maxlen = max(maxlen, 22) # avoid truncating hash
946 950 return util.ellipsis(line, maxlen)
947 951
948 952 def ruleeditor(repo, ui, rules, editcomment=""):
949 953 """open an editor to edit rules
950 954
951 955 rules are in the format [ [act, ctx], ...] like in state.rules
952 956 """
953 957 rules = '\n'.join([makedesc(repo, act, rev) for [act, rev] in rules])
954 958 rules += '\n\n'
955 959 rules += editcomment
956 960 rules = ui.edit(rules, ui.username())
957 961
958 962 # Save edit rules in .hg/histedit-last-edit.txt in case
959 963 # the user needs to ask for help after something
960 964 # surprising happens.
961 965 f = open(repo.join('histedit-last-edit.txt'), 'w')
962 966 f.write(rules)
963 967 f.close()
964 968
965 969 return rules
966 970
967 971 def verifyrules(rules, repo, ctxs):
968 972 """Verify that there exists exactly one edit rule per given changeset.
969 973
970 974 Will abort if there are to many or too few rules, a malformed rule,
971 975 or a rule on a changeset outside of the user-given range.
972 976 """
973 977 parsed = []
974 978 expected = set(c.hex() for c in ctxs)
975 979 seen = set()
976 980 for r in rules:
977 981 if ' ' not in r:
978 982 raise util.Abort(_('malformed line "%s"') % r)
979 983 action, rest = r.split(' ', 1)
980 984 ha = rest.strip().split(' ', 1)[0]
981 985 try:
982 986 ha = repo[ha].hex()
983 987 except error.RepoError:
984 988 raise util.Abort(_('unknown changeset %s listed') % ha[:12])
985 989 if ha not in expected:
986 990 raise util.Abort(
987 991 _('may not use changesets other than the ones listed'))
988 992 if ha in seen:
989 993 raise util.Abort(_('duplicated command for changeset %s') %
990 994 ha[:12])
991 995 seen.add(ha)
992 996 if action not in actiontable:
993 997 raise util.Abort(_('unknown action "%s"') % action)
994 998 parsed.append([action, ha])
995 999 missing = sorted(expected - seen) # sort to stabilize output
996 1000 if missing:
997 1001 raise util.Abort(_('missing rules for changeset %s') %
998 1002 missing[0][:12],
999 1003 hint=_('do you want to use the drop action?'))
1000 1004 return parsed
1001 1005
1002 1006 def processreplacement(state):
1003 1007 """process the list of replacements to return
1004 1008
1005 1009 1) the final mapping between original and created nodes
1006 1010 2) the list of temporary node created by histedit
1007 1011 3) the list of new commit created by histedit"""
1008 1012 replacements = state.replacements
1009 1013 allsuccs = set()
1010 1014 replaced = set()
1011 1015 fullmapping = {}
1012 1016 # initialise basic set
1013 1017 # fullmapping record all operation recorded in replacement
1014 1018 for rep in replacements:
1015 1019 allsuccs.update(rep[1])
1016 1020 replaced.add(rep[0])
1017 1021 fullmapping.setdefault(rep[0], set()).update(rep[1])
1018 1022 new = allsuccs - replaced
1019 1023 tmpnodes = allsuccs & replaced
1020 1024 # Reduce content fullmapping into direct relation between original nodes
1021 1025 # and final node created during history edition
1022 1026 # Dropped changeset are replaced by an empty list
1023 1027 toproceed = set(fullmapping)
1024 1028 final = {}
1025 1029 while toproceed:
1026 1030 for x in list(toproceed):
1027 1031 succs = fullmapping[x]
1028 1032 for s in list(succs):
1029 1033 if s in toproceed:
1030 1034 # non final node with unknown closure
1031 1035 # We can't process this now
1032 1036 break
1033 1037 elif s in final:
1034 1038 # non final node, replace with closure
1035 1039 succs.remove(s)
1036 1040 succs.update(final[s])
1037 1041 else:
1038 1042 final[x] = succs
1039 1043 toproceed.remove(x)
1040 1044 # remove tmpnodes from final mapping
1041 1045 for n in tmpnodes:
1042 1046 del final[n]
1043 1047 # we expect all changes involved in final to exist in the repo
1044 1048 # turn `final` into list (topologically sorted)
1045 1049 nm = state.repo.changelog.nodemap
1046 1050 for prec, succs in final.items():
1047 1051 final[prec] = sorted(succs, key=nm.get)
1048 1052
1049 1053 # computed topmost element (necessary for bookmark)
1050 1054 if new:
1051 1055 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1052 1056 elif not final:
1053 1057 # Nothing rewritten at all. we won't need `newtopmost`
1054 1058 # It is the same as `oldtopmost` and `processreplacement` know it
1055 1059 newtopmost = None
1056 1060 else:
1057 1061 # every body died. The newtopmost is the parent of the root.
1058 1062 r = state.repo.changelog.rev
1059 1063 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1060 1064
1061 1065 return final, tmpnodes, new, newtopmost
1062 1066
1063 1067 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1064 1068 """Move bookmark from old to newly created node"""
1065 1069 if not mapping:
1066 1070 # if nothing got rewritten there is not purpose for this function
1067 1071 return
1068 1072 moves = []
1069 1073 for bk, old in sorted(repo._bookmarks.iteritems()):
1070 1074 if old == oldtopmost:
1071 1075 # special case ensure bookmark stay on tip.
1072 1076 #
1073 1077 # This is arguably a feature and we may only want that for the
1074 1078 # active bookmark. But the behavior is kept compatible with the old
1075 1079 # version for now.
1076 1080 moves.append((bk, newtopmost))
1077 1081 continue
1078 1082 base = old
1079 1083 new = mapping.get(base, None)
1080 1084 if new is None:
1081 1085 continue
1082 1086 while not new:
1083 1087 # base is killed, trying with parent
1084 1088 base = repo[base].p1().node()
1085 1089 new = mapping.get(base, (base,))
1086 1090 # nothing to move
1087 1091 moves.append((bk, new[-1]))
1088 1092 if moves:
1089 1093 marks = repo._bookmarks
1090 1094 for mark, new in moves:
1091 1095 old = marks[mark]
1092 1096 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1093 1097 % (mark, node.short(old), node.short(new)))
1094 1098 marks[mark] = new
1095 1099 marks.write()
1096 1100
1097 1101 def cleanupnode(ui, repo, name, nodes):
1098 1102 """strip a group of nodes from the repository
1099 1103
1100 1104 The set of node to strip may contains unknown nodes."""
1101 1105 ui.debug('should strip %s nodes %s\n' %
1102 1106 (name, ', '.join([node.short(n) for n in nodes])))
1103 1107 lock = None
1104 1108 try:
1105 1109 lock = repo.lock()
1106 1110 # Find all node that need to be stripped
1107 1111 # (we hg %lr instead of %ln to silently ignore unknown item
1108 1112 nm = repo.changelog.nodemap
1109 1113 nodes = sorted(n for n in nodes if n in nm)
1110 1114 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1111 1115 for c in roots:
1112 1116 # We should process node in reverse order to strip tip most first.
1113 1117 # but this trigger a bug in changegroup hook.
1114 1118 # This would reduce bundle overhead
1115 1119 repair.strip(ui, repo, c)
1116 1120 finally:
1117 1121 release(lock)
1118 1122
1119 1123 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1120 1124 if isinstance(nodelist, str):
1121 1125 nodelist = [nodelist]
1122 1126 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1123 1127 state = histeditstate(repo)
1124 1128 state.read()
1125 1129 histedit_nodes = set([repo[rulehash].node() for (action, rulehash)
1126 1130 in state.rules if rulehash in repo])
1127 1131 strip_nodes = set([repo[n].node() for n in nodelist])
1128 1132 common_nodes = histedit_nodes & strip_nodes
1129 1133 if common_nodes:
1130 1134 raise util.Abort(_("histedit in progress, can't strip %s")
1131 1135 % ', '.join(node.short(x) for x in common_nodes))
1132 1136 return orig(ui, repo, nodelist, *args, **kwargs)
1133 1137
1134 1138 extensions.wrapfunction(repair, 'strip', stripwrapper)
1135 1139
1136 1140 def summaryhook(ui, repo):
1137 1141 if not os.path.exists(repo.join('histedit-state')):
1138 1142 return
1139 1143 state = histeditstate(repo)
1140 1144 state.read()
1141 1145 if state.rules:
1142 1146 # i18n: column positioning for "hg summary"
1143 1147 ui.write(_('hist: %s (histedit --continue)\n') %
1144 1148 (ui.label(_('%d remaining'), 'histedit.remaining') %
1145 1149 len(state.rules)))
1146 1150
1147 1151 def extsetup(ui):
1148 1152 cmdutil.summaryhooks.add('histedit', summaryhook)
1149 1153 cmdutil.unfinishedstates.append(
1150 1154 ['histedit-state', False, True, _('histedit in progress'),
1151 1155 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
@@ -1,742 +1,746 b''
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007-2015 Christian Ebert <blacktrash@gmx.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 # $Id$
9 9 #
10 10 # Keyword expansion hack against the grain of a Distributed SCM
11 11 #
12 12 # There are many good reasons why this is not needed in a distributed
13 13 # SCM, still it may be useful in very small projects based on single
14 14 # files (like LaTeX packages), that are mostly addressed to an
15 15 # audience not running a version control system.
16 16 #
17 17 # For in-depth discussion refer to
18 18 # <http://mercurial.selenic.com/wiki/KeywordPlan>.
19 19 #
20 20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 21 #
22 22 # Binary files are not touched.
23 23 #
24 24 # Files to act upon/ignore are specified in the [keyword] section.
25 25 # Customized keyword template mappings in the [keywordmaps] section.
26 26 #
27 27 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
28 28
29 29 '''expand keywords in tracked files
30 30
31 31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
32 32 tracked text files selected by your configuration.
33 33
34 34 Keywords are only expanded in local repositories and not stored in the
35 35 change history. The mechanism can be regarded as a convenience for the
36 36 current user or for archive distribution.
37 37
38 38 Keywords expand to the changeset data pertaining to the latest change
39 39 relative to the working directory parent of each file.
40 40
41 41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
42 42 sections of hgrc files.
43 43
44 44 Example::
45 45
46 46 [keyword]
47 47 # expand keywords in every python file except those matching "x*"
48 48 **.py =
49 49 x* = ignore
50 50
51 51 [keywordset]
52 52 # prefer svn- over cvs-like default keywordmaps
53 53 svn = True
54 54
55 55 .. note::
56 56
57 57 The more specific you are in your filename patterns the less you
58 58 lose speed in huge repositories.
59 59
60 60 For [keywordmaps] template mapping and expansion demonstration and
61 61 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
62 62 available templates and filters.
63 63
64 64 Three additional date template filters are provided:
65 65
66 66 :``utcdate``: "2006/09/18 15:13:13"
67 67 :``svnutcdate``: "2006-09-18 15:13:13Z"
68 68 :``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
69 69
70 70 The default template mappings (view with :hg:`kwdemo -d`) can be
71 71 replaced with customized keywords and templates. Again, run
72 72 :hg:`kwdemo` to control the results of your configuration changes.
73 73
74 74 Before changing/disabling active keywords, you must run :hg:`kwshrink`
75 75 to avoid storing expanded keywords in the change history.
76 76
77 77 To force expansion after enabling it, or a configuration change, run
78 78 :hg:`kwexpand`.
79 79
80 80 Expansions spanning more than one line and incremental expansions,
81 81 like CVS' $Log$, are not supported. A keyword template map "Log =
82 82 {desc}" expands to the first line of the changeset description.
83 83 '''
84 84
85 85 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
86 86 from mercurial import localrepo, match, patch, templatefilters, util
87 87 from mercurial import scmutil, pathutil
88 88 from mercurial.hgweb import webcommands
89 89 from mercurial.i18n import _
90 90 import os, re, tempfile
91 91
92 92 cmdtable = {}
93 93 command = cmdutil.command(cmdtable)
94 # Note for extension authors: ONLY specify testedwith = 'internal' for
95 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
96 # be specifying the version(s) of Mercurial they are tested with, or
97 # leave the attribute unspecified.
94 98 testedwith = 'internal'
95 99
96 100 # hg commands that do not act on keywords
97 101 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
98 102 ' outgoing push tip verify convert email glog')
99 103
100 104 # hg commands that trigger expansion only when writing to working dir,
101 105 # not when reading filelog, and unexpand when reading from working dir
102 106 restricted = ('merge kwexpand kwshrink record qrecord resolve transplant'
103 107 ' unshelve rebase graft backout histedit fetch')
104 108
105 109 # names of extensions using dorecord
106 110 recordextensions = 'record'
107 111
108 112 colortable = {
109 113 'kwfiles.enabled': 'green bold',
110 114 'kwfiles.deleted': 'cyan bold underline',
111 115 'kwfiles.enabledunknown': 'green',
112 116 'kwfiles.ignored': 'bold',
113 117 'kwfiles.ignoredunknown': 'none'
114 118 }
115 119
116 120 # date like in cvs' $Date
117 121 def utcdate(text):
118 122 ''':utcdate: Date. Returns a UTC-date in this format: "2009/08/18 11:00:13".
119 123 '''
120 124 return util.datestr((util.parsedate(text)[0], 0), '%Y/%m/%d %H:%M:%S')
121 125 # date like in svn's $Date
122 126 def svnisodate(text):
123 127 ''':svnisodate: Date. Returns a date in this format: "2009-08-18 13:00:13
124 128 +0200 (Tue, 18 Aug 2009)".
125 129 '''
126 130 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
127 131 # date like in svn's $Id
128 132 def svnutcdate(text):
129 133 ''':svnutcdate: Date. Returns a UTC-date in this format: "2009-08-18
130 134 11:00:13Z".
131 135 '''
132 136 return util.datestr((util.parsedate(text)[0], 0), '%Y-%m-%d %H:%M:%SZ')
133 137
134 138 templatefilters.filters.update({'utcdate': utcdate,
135 139 'svnisodate': svnisodate,
136 140 'svnutcdate': svnutcdate})
137 141
138 142 # make keyword tools accessible
139 143 kwtools = {'templater': None, 'hgcmd': ''}
140 144
141 145 def _defaultkwmaps(ui):
142 146 '''Returns default keywordmaps according to keywordset configuration.'''
143 147 templates = {
144 148 'Revision': '{node|short}',
145 149 'Author': '{author|user}',
146 150 }
147 151 kwsets = ({
148 152 'Date': '{date|utcdate}',
149 153 'RCSfile': '{file|basename},v',
150 154 'RCSFile': '{file|basename},v', # kept for backwards compatibility
151 155 # with hg-keyword
152 156 'Source': '{root}/{file},v',
153 157 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
154 158 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
155 159 }, {
156 160 'Date': '{date|svnisodate}',
157 161 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
158 162 'LastChangedRevision': '{node|short}',
159 163 'LastChangedBy': '{author|user}',
160 164 'LastChangedDate': '{date|svnisodate}',
161 165 })
162 166 templates.update(kwsets[ui.configbool('keywordset', 'svn')])
163 167 return templates
164 168
165 169 def _shrinktext(text, subfunc):
166 170 '''Helper for keyword expansion removal in text.
167 171 Depending on subfunc also returns number of substitutions.'''
168 172 return subfunc(r'$\1$', text)
169 173
170 174 def _preselect(wstatus, changed):
171 175 '''Retrieves modified and added files from a working directory state
172 176 and returns the subset of each contained in given changed files
173 177 retrieved from a change context.'''
174 178 modified = [f for f in wstatus.modified if f in changed]
175 179 added = [f for f in wstatus.added if f in changed]
176 180 return modified, added
177 181
178 182
179 183 class kwtemplater(object):
180 184 '''
181 185 Sets up keyword templates, corresponding keyword regex, and
182 186 provides keyword substitution functions.
183 187 '''
184 188
185 189 def __init__(self, ui, repo, inc, exc):
186 190 self.ui = ui
187 191 self.repo = repo
188 192 self.match = match.match(repo.root, '', [], inc, exc)
189 193 self.restrict = kwtools['hgcmd'] in restricted.split()
190 194 self.postcommit = False
191 195
192 196 kwmaps = self.ui.configitems('keywordmaps')
193 197 if kwmaps: # override default templates
194 198 self.templates = dict(kwmaps)
195 199 else:
196 200 self.templates = _defaultkwmaps(self.ui)
197 201
198 202 @util.propertycache
199 203 def escape(self):
200 204 '''Returns bar-separated and escaped keywords.'''
201 205 return '|'.join(map(re.escape, self.templates.keys()))
202 206
203 207 @util.propertycache
204 208 def rekw(self):
205 209 '''Returns regex for unexpanded keywords.'''
206 210 return re.compile(r'\$(%s)\$' % self.escape)
207 211
208 212 @util.propertycache
209 213 def rekwexp(self):
210 214 '''Returns regex for expanded keywords.'''
211 215 return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
212 216
213 217 def substitute(self, data, path, ctx, subfunc):
214 218 '''Replaces keywords in data with expanded template.'''
215 219 def kwsub(mobj):
216 220 kw = mobj.group(1)
217 221 ct = cmdutil.changeset_templater(self.ui, self.repo, False, None,
218 222 self.templates[kw], '', False)
219 223 self.ui.pushbuffer()
220 224 ct.show(ctx, root=self.repo.root, file=path)
221 225 ekw = templatefilters.firstline(self.ui.popbuffer())
222 226 return '$%s: %s $' % (kw, ekw)
223 227 return subfunc(kwsub, data)
224 228
225 229 def linkctx(self, path, fileid):
226 230 '''Similar to filelog.linkrev, but returns a changectx.'''
227 231 return self.repo.filectx(path, fileid=fileid).changectx()
228 232
229 233 def expand(self, path, node, data):
230 234 '''Returns data with keywords expanded.'''
231 235 if not self.restrict and self.match(path) and not util.binary(data):
232 236 ctx = self.linkctx(path, node)
233 237 return self.substitute(data, path, ctx, self.rekw.sub)
234 238 return data
235 239
236 240 def iskwfile(self, cand, ctx):
237 241 '''Returns subset of candidates which are configured for keyword
238 242 expansion but are not symbolic links.'''
239 243 return [f for f in cand if self.match(f) and 'l' not in ctx.flags(f)]
240 244
241 245 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
242 246 '''Overwrites selected files expanding/shrinking keywords.'''
243 247 if self.restrict or lookup or self.postcommit: # exclude kw_copy
244 248 candidates = self.iskwfile(candidates, ctx)
245 249 if not candidates:
246 250 return
247 251 kwcmd = self.restrict and lookup # kwexpand/kwshrink
248 252 if self.restrict or expand and lookup:
249 253 mf = ctx.manifest()
250 254 if self.restrict or rekw:
251 255 re_kw = self.rekw
252 256 else:
253 257 re_kw = self.rekwexp
254 258 if expand:
255 259 msg = _('overwriting %s expanding keywords\n')
256 260 else:
257 261 msg = _('overwriting %s shrinking keywords\n')
258 262 for f in candidates:
259 263 if self.restrict:
260 264 data = self.repo.file(f).read(mf[f])
261 265 else:
262 266 data = self.repo.wread(f)
263 267 if util.binary(data):
264 268 continue
265 269 if expand:
266 270 parents = ctx.parents()
267 271 if lookup:
268 272 ctx = self.linkctx(f, mf[f])
269 273 elif self.restrict and len(parents) > 1:
270 274 # merge commit
271 275 # in case of conflict f is in modified state during
272 276 # merge, even if f does not differ from f in parent
273 277 for p in parents:
274 278 if f in p and not p[f].cmp(ctx[f]):
275 279 ctx = p[f].changectx()
276 280 break
277 281 data, found = self.substitute(data, f, ctx, re_kw.subn)
278 282 elif self.restrict:
279 283 found = re_kw.search(data)
280 284 else:
281 285 data, found = _shrinktext(data, re_kw.subn)
282 286 if found:
283 287 self.ui.note(msg % f)
284 288 fp = self.repo.wvfs(f, "wb", atomictemp=True)
285 289 fp.write(data)
286 290 fp.close()
287 291 if kwcmd:
288 292 self.repo.dirstate.normal(f)
289 293 elif self.postcommit:
290 294 self.repo.dirstate.normallookup(f)
291 295
292 296 def shrink(self, fname, text):
293 297 '''Returns text with all keyword substitutions removed.'''
294 298 if self.match(fname) and not util.binary(text):
295 299 return _shrinktext(text, self.rekwexp.sub)
296 300 return text
297 301
298 302 def shrinklines(self, fname, lines):
299 303 '''Returns lines with keyword substitutions removed.'''
300 304 if self.match(fname):
301 305 text = ''.join(lines)
302 306 if not util.binary(text):
303 307 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
304 308 return lines
305 309
306 310 def wread(self, fname, data):
307 311 '''If in restricted mode returns data read from wdir with
308 312 keyword substitutions removed.'''
309 313 if self.restrict:
310 314 return self.shrink(fname, data)
311 315 return data
312 316
313 317 class kwfilelog(filelog.filelog):
314 318 '''
315 319 Subclass of filelog to hook into its read, add, cmp methods.
316 320 Keywords are "stored" unexpanded, and processed on reading.
317 321 '''
318 322 def __init__(self, opener, kwt, path):
319 323 super(kwfilelog, self).__init__(opener, path)
320 324 self.kwt = kwt
321 325 self.path = path
322 326
323 327 def read(self, node):
324 328 '''Expands keywords when reading filelog.'''
325 329 data = super(kwfilelog, self).read(node)
326 330 if self.renamed(node):
327 331 return data
328 332 return self.kwt.expand(self.path, node, data)
329 333
330 334 def add(self, text, meta, tr, link, p1=None, p2=None):
331 335 '''Removes keyword substitutions when adding to filelog.'''
332 336 text = self.kwt.shrink(self.path, text)
333 337 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
334 338
335 339 def cmp(self, node, text):
336 340 '''Removes keyword substitutions for comparison.'''
337 341 text = self.kwt.shrink(self.path, text)
338 342 return super(kwfilelog, self).cmp(node, text)
339 343
340 344 def _status(ui, repo, wctx, kwt, *pats, **opts):
341 345 '''Bails out if [keyword] configuration is not active.
342 346 Returns status of working directory.'''
343 347 if kwt:
344 348 return repo.status(match=scmutil.match(wctx, pats, opts), clean=True,
345 349 unknown=opts.get('unknown') or opts.get('all'))
346 350 if ui.configitems('keyword'):
347 351 raise util.Abort(_('[keyword] patterns cannot match'))
348 352 raise util.Abort(_('no [keyword] patterns configured'))
349 353
350 354 def _kwfwrite(ui, repo, expand, *pats, **opts):
351 355 '''Selects files and passes them to kwtemplater.overwrite.'''
352 356 wctx = repo[None]
353 357 if len(wctx.parents()) > 1:
354 358 raise util.Abort(_('outstanding uncommitted merge'))
355 359 kwt = kwtools['templater']
356 360 wlock = repo.wlock()
357 361 try:
358 362 status = _status(ui, repo, wctx, kwt, *pats, **opts)
359 363 if status.modified or status.added or status.removed or status.deleted:
360 364 raise util.Abort(_('outstanding uncommitted changes'))
361 365 kwt.overwrite(wctx, status.clean, True, expand)
362 366 finally:
363 367 wlock.release()
364 368
365 369 @command('kwdemo',
366 370 [('d', 'default', None, _('show default keyword template maps')),
367 371 ('f', 'rcfile', '',
368 372 _('read maps from rcfile'), _('FILE'))],
369 373 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'),
370 374 optionalrepo=True)
371 375 def demo(ui, repo, *args, **opts):
372 376 '''print [keywordmaps] configuration and an expansion example
373 377
374 378 Show current, custom, or default keyword template maps and their
375 379 expansions.
376 380
377 381 Extend the current configuration by specifying maps as arguments
378 382 and using -f/--rcfile to source an external hgrc file.
379 383
380 384 Use -d/--default to disable current configuration.
381 385
382 386 See :hg:`help templates` for information on templates and filters.
383 387 '''
384 388 def demoitems(section, items):
385 389 ui.write('[%s]\n' % section)
386 390 for k, v in sorted(items):
387 391 ui.write('%s = %s\n' % (k, v))
388 392
389 393 fn = 'demo.txt'
390 394 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
391 395 ui.note(_('creating temporary repository at %s\n') % tmpdir)
392 396 repo = localrepo.localrepository(repo.baseui, tmpdir, True)
393 397 ui.setconfig('keyword', fn, '', 'keyword')
394 398 svn = ui.configbool('keywordset', 'svn')
395 399 # explicitly set keywordset for demo output
396 400 ui.setconfig('keywordset', 'svn', svn, 'keyword')
397 401
398 402 uikwmaps = ui.configitems('keywordmaps')
399 403 if args or opts.get('rcfile'):
400 404 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
401 405 if uikwmaps:
402 406 ui.status(_('\textending current template maps\n'))
403 407 if opts.get('default') or not uikwmaps:
404 408 if svn:
405 409 ui.status(_('\toverriding default svn keywordset\n'))
406 410 else:
407 411 ui.status(_('\toverriding default cvs keywordset\n'))
408 412 if opts.get('rcfile'):
409 413 ui.readconfig(opts.get('rcfile'))
410 414 if args:
411 415 # simulate hgrc parsing
412 416 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
413 417 fp = repo.vfs('hgrc', 'w')
414 418 fp.writelines(rcmaps)
415 419 fp.close()
416 420 ui.readconfig(repo.join('hgrc'))
417 421 kwmaps = dict(ui.configitems('keywordmaps'))
418 422 elif opts.get('default'):
419 423 if svn:
420 424 ui.status(_('\n\tconfiguration using default svn keywordset\n'))
421 425 else:
422 426 ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
423 427 kwmaps = _defaultkwmaps(ui)
424 428 if uikwmaps:
425 429 ui.status(_('\tdisabling current template maps\n'))
426 430 for k, v in kwmaps.iteritems():
427 431 ui.setconfig('keywordmaps', k, v, 'keyword')
428 432 else:
429 433 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
430 434 if uikwmaps:
431 435 kwmaps = dict(uikwmaps)
432 436 else:
433 437 kwmaps = _defaultkwmaps(ui)
434 438
435 439 uisetup(ui)
436 440 reposetup(ui, repo)
437 441 ui.write('[extensions]\nkeyword =\n')
438 442 demoitems('keyword', ui.configitems('keyword'))
439 443 demoitems('keywordset', ui.configitems('keywordset'))
440 444 demoitems('keywordmaps', kwmaps.iteritems())
441 445 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
442 446 repo.wvfs.write(fn, keywords)
443 447 repo[None].add([fn])
444 448 ui.note(_('\nkeywords written to %s:\n') % fn)
445 449 ui.note(keywords)
446 450 wlock = repo.wlock()
447 451 try:
448 452 repo.dirstate.setbranch('demobranch')
449 453 finally:
450 454 wlock.release()
451 455 for name, cmd in ui.configitems('hooks'):
452 456 if name.split('.', 1)[0].find('commit') > -1:
453 457 repo.ui.setconfig('hooks', name, '', 'keyword')
454 458 msg = _('hg keyword configuration and expansion example')
455 459 ui.note(("hg ci -m '%s'\n" % msg))
456 460 repo.commit(text=msg)
457 461 ui.status(_('\n\tkeywords expanded\n'))
458 462 ui.write(repo.wread(fn))
459 463 repo.wvfs.rmtree(repo.root)
460 464
461 465 @command('kwexpand',
462 466 commands.walkopts,
463 467 _('hg kwexpand [OPTION]... [FILE]...'),
464 468 inferrepo=True)
465 469 def expand(ui, repo, *pats, **opts):
466 470 '''expand keywords in the working directory
467 471
468 472 Run after (re)enabling keyword expansion.
469 473
470 474 kwexpand refuses to run if given files contain local changes.
471 475 '''
472 476 # 3rd argument sets expansion to True
473 477 _kwfwrite(ui, repo, True, *pats, **opts)
474 478
475 479 @command('kwfiles',
476 480 [('A', 'all', None, _('show keyword status flags of all files')),
477 481 ('i', 'ignore', None, _('show files excluded from expansion')),
478 482 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
479 483 ] + commands.walkopts,
480 484 _('hg kwfiles [OPTION]... [FILE]...'),
481 485 inferrepo=True)
482 486 def files(ui, repo, *pats, **opts):
483 487 '''show files configured for keyword expansion
484 488
485 489 List which files in the working directory are matched by the
486 490 [keyword] configuration patterns.
487 491
488 492 Useful to prevent inadvertent keyword expansion and to speed up
489 493 execution by including only files that are actual candidates for
490 494 expansion.
491 495
492 496 See :hg:`help keyword` on how to construct patterns both for
493 497 inclusion and exclusion of files.
494 498
495 499 With -A/--all and -v/--verbose the codes used to show the status
496 500 of files are::
497 501
498 502 K = keyword expansion candidate
499 503 k = keyword expansion candidate (not tracked)
500 504 I = ignored
501 505 i = ignored (not tracked)
502 506 '''
503 507 kwt = kwtools['templater']
504 508 wctx = repo[None]
505 509 status = _status(ui, repo, wctx, kwt, *pats, **opts)
506 510 if pats:
507 511 cwd = repo.getcwd()
508 512 else:
509 513 cwd = ''
510 514 files = []
511 515 if not opts.get('unknown') or opts.get('all'):
512 516 files = sorted(status.modified + status.added + status.clean)
513 517 kwfiles = kwt.iskwfile(files, wctx)
514 518 kwdeleted = kwt.iskwfile(status.deleted, wctx)
515 519 kwunknown = kwt.iskwfile(status.unknown, wctx)
516 520 if not opts.get('ignore') or opts.get('all'):
517 521 showfiles = kwfiles, kwdeleted, kwunknown
518 522 else:
519 523 showfiles = [], [], []
520 524 if opts.get('all') or opts.get('ignore'):
521 525 showfiles += ([f for f in files if f not in kwfiles],
522 526 [f for f in status.unknown if f not in kwunknown])
523 527 kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
524 528 kwstates = zip(kwlabels, 'K!kIi', showfiles)
525 529 fm = ui.formatter('kwfiles', opts)
526 530 fmt = '%.0s%s\n'
527 531 if opts.get('all') or ui.verbose:
528 532 fmt = '%s %s\n'
529 533 for kwstate, char, filenames in kwstates:
530 534 label = 'kwfiles.' + kwstate
531 535 for f in filenames:
532 536 fm.startitem()
533 537 fm.write('kwstatus path', fmt, char,
534 538 repo.pathto(f, cwd), label=label)
535 539 fm.end()
536 540
537 541 @command('kwshrink',
538 542 commands.walkopts,
539 543 _('hg kwshrink [OPTION]... [FILE]...'),
540 544 inferrepo=True)
541 545 def shrink(ui, repo, *pats, **opts):
542 546 '''revert expanded keywords in the working directory
543 547
544 548 Must be run before changing/disabling active keywords.
545 549
546 550 kwshrink refuses to run if given files contain local changes.
547 551 '''
548 552 # 3rd argument sets expansion to False
549 553 _kwfwrite(ui, repo, False, *pats, **opts)
550 554
551 555
552 556 def uisetup(ui):
553 557 ''' Monkeypatches dispatch._parse to retrieve user command.'''
554 558
555 559 def kwdispatch_parse(orig, ui, args):
556 560 '''Monkeypatch dispatch._parse to obtain running hg command.'''
557 561 cmd, func, args, options, cmdoptions = orig(ui, args)
558 562 kwtools['hgcmd'] = cmd
559 563 return cmd, func, args, options, cmdoptions
560 564
561 565 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
562 566
563 567 def reposetup(ui, repo):
564 568 '''Sets up repo as kwrepo for keyword substitution.
565 569 Overrides file method to return kwfilelog instead of filelog
566 570 if file matches user configuration.
567 571 Wraps commit to overwrite configured files with updated
568 572 keyword substitutions.
569 573 Monkeypatches patch and webcommands.'''
570 574
571 575 try:
572 576 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
573 577 or '.hg' in util.splitpath(repo.root)
574 578 or repo._url.startswith('bundle:')):
575 579 return
576 580 except AttributeError:
577 581 pass
578 582
579 583 inc, exc = [], ['.hg*']
580 584 for pat, opt in ui.configitems('keyword'):
581 585 if opt != 'ignore':
582 586 inc.append(pat)
583 587 else:
584 588 exc.append(pat)
585 589 if not inc:
586 590 return
587 591
588 592 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
589 593
590 594 class kwrepo(repo.__class__):
591 595 def file(self, f):
592 596 if f[0] == '/':
593 597 f = f[1:]
594 598 return kwfilelog(self.svfs, kwt, f)
595 599
596 600 def wread(self, filename):
597 601 data = super(kwrepo, self).wread(filename)
598 602 return kwt.wread(filename, data)
599 603
600 604 def commit(self, *args, **opts):
601 605 # use custom commitctx for user commands
602 606 # other extensions can still wrap repo.commitctx directly
603 607 self.commitctx = self.kwcommitctx
604 608 try:
605 609 return super(kwrepo, self).commit(*args, **opts)
606 610 finally:
607 611 del self.commitctx
608 612
609 613 def kwcommitctx(self, ctx, error=False):
610 614 n = super(kwrepo, self).commitctx(ctx, error)
611 615 # no lock needed, only called from repo.commit() which already locks
612 616 if not kwt.postcommit:
613 617 restrict = kwt.restrict
614 618 kwt.restrict = True
615 619 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
616 620 False, True)
617 621 kwt.restrict = restrict
618 622 return n
619 623
620 624 def rollback(self, dryrun=False, force=False):
621 625 wlock = self.wlock()
622 626 try:
623 627 if not dryrun:
624 628 changed = self['.'].files()
625 629 ret = super(kwrepo, self).rollback(dryrun, force)
626 630 if not dryrun:
627 631 ctx = self['.']
628 632 modified, added = _preselect(ctx.status(), changed)
629 633 kwt.overwrite(ctx, modified, True, True)
630 634 kwt.overwrite(ctx, added, True, False)
631 635 return ret
632 636 finally:
633 637 wlock.release()
634 638
635 639 # monkeypatches
636 640 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
637 641 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
638 642 rejects or conflicts due to expanded keywords in working dir.'''
639 643 orig(self, ui, gp, backend, store, eolmode)
640 644 # shrink keywords read from working dir
641 645 self.lines = kwt.shrinklines(self.fname, self.lines)
642 646
643 647 def kwdiff(orig, *args, **kwargs):
644 648 '''Monkeypatch patch.diff to avoid expansion.'''
645 649 kwt.restrict = True
646 650 return orig(*args, **kwargs)
647 651
648 652 def kwweb_skip(orig, web, req, tmpl):
649 653 '''Wraps webcommands.x turning off keyword expansion.'''
650 654 kwt.match = util.never
651 655 return orig(web, req, tmpl)
652 656
653 657 def kw_amend(orig, ui, repo, commitfunc, old, extra, pats, opts):
654 658 '''Wraps cmdutil.amend expanding keywords after amend.'''
655 659 wlock = repo.wlock()
656 660 try:
657 661 kwt.postcommit = True
658 662 newid = orig(ui, repo, commitfunc, old, extra, pats, opts)
659 663 if newid != old.node():
660 664 ctx = repo[newid]
661 665 kwt.restrict = True
662 666 kwt.overwrite(ctx, ctx.files(), False, True)
663 667 kwt.restrict = False
664 668 return newid
665 669 finally:
666 670 wlock.release()
667 671
668 672 def kw_copy(orig, ui, repo, pats, opts, rename=False):
669 673 '''Wraps cmdutil.copy so that copy/rename destinations do not
670 674 contain expanded keywords.
671 675 Note that the source of a regular file destination may also be a
672 676 symlink:
673 677 hg cp sym x -> x is symlink
674 678 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
675 679 For the latter we have to follow the symlink to find out whether its
676 680 target is configured for expansion and we therefore must unexpand the
677 681 keywords in the destination.'''
678 682 wlock = repo.wlock()
679 683 try:
680 684 orig(ui, repo, pats, opts, rename)
681 685 if opts.get('dry_run'):
682 686 return
683 687 wctx = repo[None]
684 688 cwd = repo.getcwd()
685 689
686 690 def haskwsource(dest):
687 691 '''Returns true if dest is a regular file and configured for
688 692 expansion or a symlink which points to a file configured for
689 693 expansion. '''
690 694 source = repo.dirstate.copied(dest)
691 695 if 'l' in wctx.flags(source):
692 696 source = pathutil.canonpath(repo.root, cwd,
693 697 os.path.realpath(source))
694 698 return kwt.match(source)
695 699
696 700 candidates = [f for f in repo.dirstate.copies() if
697 701 'l' not in wctx.flags(f) and haskwsource(f)]
698 702 kwt.overwrite(wctx, candidates, False, False)
699 703 finally:
700 704 wlock.release()
701 705
702 706 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
703 707 '''Wraps record.dorecord expanding keywords after recording.'''
704 708 wlock = repo.wlock()
705 709 try:
706 710 # record returns 0 even when nothing has changed
707 711 # therefore compare nodes before and after
708 712 kwt.postcommit = True
709 713 ctx = repo['.']
710 714 wstatus = ctx.status()
711 715 ret = orig(ui, repo, commitfunc, *pats, **opts)
712 716 recctx = repo['.']
713 717 if ctx != recctx:
714 718 modified, added = _preselect(wstatus, recctx.files())
715 719 kwt.restrict = False
716 720 kwt.overwrite(recctx, modified, False, True)
717 721 kwt.overwrite(recctx, added, False, True, True)
718 722 kwt.restrict = True
719 723 return ret
720 724 finally:
721 725 wlock.release()
722 726
723 727 def kwfilectx_cmp(orig, self, fctx):
724 728 # keyword affects data size, comparing wdir and filelog size does
725 729 # not make sense
726 730 if (fctx._filerev is None and
727 731 (self._repo._encodefilterpats or
728 732 kwt.match(fctx.path()) and 'l' not in fctx.flags() or
729 733 self.size() - 4 == fctx.size()) or
730 734 self.size() == fctx.size()):
731 735 return self._filelog.cmp(self._filenode, fctx.data())
732 736 return True
733 737
734 738 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
735 739 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
736 740 extensions.wrapfunction(patch, 'diff', kwdiff)
737 741 extensions.wrapfunction(cmdutil, 'amend', kw_amend)
738 742 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
739 743 extensions.wrapfunction(cmdutil, 'dorecord', kw_dorecord)
740 744 for c in 'annotate changeset rev filediff diff'.split():
741 745 extensions.wrapfunction(webcommands, c, kwweb_skip)
742 746 repo.__class__ = kwrepo
@@ -1,128 +1,132 b''
1 1 # Copyright 2009-2010 Gregory P. Ward
2 2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 3 # Copyright 2010-2011 Fog Creek Software
4 4 # Copyright 2010-2011 Unity Technologies
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 '''track large binary files
10 10
11 11 Large binary files tend to be not very compressible, not very
12 12 diffable, and not at all mergeable. Such files are not handled
13 13 efficiently by Mercurial's storage format (revlog), which is based on
14 14 compressed binary deltas; storing large binary files as regular
15 15 Mercurial files wastes bandwidth and disk space and increases
16 16 Mercurial's memory usage. The largefiles extension addresses these
17 17 problems by adding a centralized client-server layer on top of
18 18 Mercurial: largefiles live in a *central store* out on the network
19 19 somewhere, and you only fetch the revisions that you need when you
20 20 need them.
21 21
22 22 largefiles works by maintaining a "standin file" in .hglf/ for each
23 23 largefile. The standins are small (41 bytes: an SHA-1 hash plus
24 24 newline) and are tracked by Mercurial. Largefile revisions are
25 25 identified by the SHA-1 hash of their contents, which is written to
26 26 the standin. largefiles uses that revision ID to get/put largefile
27 27 revisions from/to the central store. This saves both disk space and
28 28 bandwidth, since you don't need to retrieve all historical revisions
29 29 of large files when you clone or pull.
30 30
31 31 To start a new repository or add new large binary files, just add
32 32 --large to your :hg:`add` command. For example::
33 33
34 34 $ dd if=/dev/urandom of=randomdata count=2000
35 35 $ hg add --large randomdata
36 36 $ hg commit -m 'add randomdata as a largefile'
37 37
38 38 When you push a changeset that adds/modifies largefiles to a remote
39 39 repository, its largefile revisions will be uploaded along with it.
40 40 Note that the remote Mercurial must also have the largefiles extension
41 41 enabled for this to work.
42 42
43 43 When you pull a changeset that affects largefiles from a remote
44 44 repository, the largefiles for the changeset will by default not be
45 45 pulled down. However, when you update to such a revision, any
46 46 largefiles needed by that revision are downloaded and cached (if
47 47 they have never been downloaded before). One way to pull largefiles
48 48 when pulling is thus to use --update, which will update your working
49 49 copy to the latest pulled revision (and thereby downloading any new
50 50 largefiles).
51 51
52 52 If you want to pull largefiles you don't need for update yet, then
53 53 you can use pull with the `--lfrev` option or the :hg:`lfpull` command.
54 54
55 55 If you know you are pulling from a non-default location and want to
56 56 download all the largefiles that correspond to the new changesets at
57 57 the same time, then you can pull with `--lfrev "pulled()"`.
58 58
59 59 If you just want to ensure that you will have the largefiles needed to
60 60 merge or rebase with new heads that you are pulling, then you can pull
61 61 with `--lfrev "head(pulled())"` flag to pre-emptively download any largefiles
62 62 that are new in the heads you are pulling.
63 63
64 64 Keep in mind that network access may now be required to update to
65 65 changesets that you have not previously updated to. The nature of the
66 66 largefiles extension means that updating is no longer guaranteed to
67 67 be a local-only operation.
68 68
69 69 If you already have large files tracked by Mercurial without the
70 70 largefiles extension, you will need to convert your repository in
71 71 order to benefit from largefiles. This is done with the
72 72 :hg:`lfconvert` command::
73 73
74 74 $ hg lfconvert --size 10 oldrepo newrepo
75 75
76 76 In repositories that already have largefiles in them, any new file
77 77 over 10MB will automatically be added as a largefile. To change this
78 78 threshold, set ``largefiles.minsize`` in your Mercurial config file
79 79 to the minimum size in megabytes to track as a largefile, or use the
80 80 --lfsize option to the add command (also in megabytes)::
81 81
82 82 [largefiles]
83 83 minsize = 2
84 84
85 85 $ hg add --lfsize 2
86 86
87 87 The ``largefiles.patterns`` config option allows you to specify a list
88 88 of filename patterns (see :hg:`help patterns`) that should always be
89 89 tracked as largefiles::
90 90
91 91 [largefiles]
92 92 patterns =
93 93 *.jpg
94 94 re:.*\.(png|bmp)$
95 95 library.zip
96 96 content/audio/*
97 97
98 98 Files that match one of these patterns will be added as largefiles
99 99 regardless of their size.
100 100
101 101 The ``largefiles.minsize`` and ``largefiles.patterns`` config options
102 102 will be ignored for any repositories not already containing a
103 103 largefile. To add the first largefile to a repository, you must
104 104 explicitly do so with the --large flag passed to the :hg:`add`
105 105 command.
106 106 '''
107 107
108 108 from mercurial import hg, localrepo
109 109
110 110 import lfcommands
111 111 import proto
112 112 import reposetup
113 113 import uisetup as uisetupmod
114 114
115 # Note for extension authors: ONLY specify testedwith = 'internal' for
116 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
117 # be specifying the version(s) of Mercurial they are tested with, or
118 # leave the attribute unspecified.
115 119 testedwith = 'internal'
116 120
117 121 reposetup = reposetup.reposetup
118 122
119 123 def featuresetup(ui, supported):
120 124 # don't die on seeing a repo with the largefiles requirement
121 125 supported |= set(['largefiles'])
122 126
123 127 def uisetup(ui):
124 128 localrepo.localrepository.featuresetupfuncs.add(featuresetup)
125 129 hg.wirepeersetupfuncs.append(proto.wirereposetup)
126 130 uisetupmod.uisetup(ui)
127 131
128 132 cmdtable = lfcommands.cmdtable
@@ -1,3579 +1,3583 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 behaviour 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 mercurial.i18n import _
66 66 from mercurial.node import bin, hex, short, nullid, nullrev
67 67 from mercurial.lock import release
68 68 from mercurial import commands, cmdutil, hg, scmutil, util, revset
69 69 from mercurial import extensions, error, phases
70 70 from mercurial import patch as patchmod
71 71 from mercurial import localrepo
72 72 from mercurial import subrepo
73 73 import os, re, errno, shutil
74 74
75 75 seriesopts = [('s', 'summary', None, _('print first line of patch header'))]
76 76
77 77 cmdtable = {}
78 78 command = cmdutil.command(cmdtable)
79 # Note for extension authors: ONLY specify testedwith = 'internal' for
80 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
81 # be specifying the version(s) of Mercurial they are tested with, or
82 # leave the attribute unspecified.
79 83 testedwith = 'internal'
80 84
81 85 # force load strip extension formerly included in mq and import some utility
82 86 try:
83 87 stripext = extensions.find('strip')
84 88 except KeyError:
85 89 # note: load is lazy so we could avoid the try-except,
86 90 # but I (marmoute) prefer this explicit code.
87 91 class dummyui(object):
88 92 def debug(self, msg):
89 93 pass
90 94 stripext = extensions.load(dummyui(), 'strip', '')
91 95
92 96 strip = stripext.strip
93 97 checksubstate = stripext.checksubstate
94 98 checklocalchanges = stripext.checklocalchanges
95 99
96 100
97 101 # Patch names looks like unix-file names.
98 102 # They must be joinable with queue directory and result in the patch path.
99 103 normname = util.normpath
100 104
101 105 class statusentry(object):
102 106 def __init__(self, node, name):
103 107 self.node, self.name = node, name
104 108 def __repr__(self):
105 109 return hex(self.node) + ':' + self.name
106 110
107 111 # The order of the headers in 'hg export' HG patches:
108 112 HGHEADERS = [
109 113 # '# HG changeset patch',
110 114 '# User ',
111 115 '# Date ',
112 116 '# ',
113 117 '# Branch ',
114 118 '# Node ID ',
115 119 '# Parent ', # can occur twice for merges - but that is not relevant for mq
116 120 ]
117 121 # The order of headers in plain 'mail style' patches:
118 122 PLAINHEADERS = {
119 123 'from': 0,
120 124 'date': 1,
121 125 'subject': 2,
122 126 }
123 127
124 128 def inserthgheader(lines, header, value):
125 129 """Assuming lines contains a HG patch header, add a header line with value.
126 130 >>> try: inserthgheader([], '# Date ', 'z')
127 131 ... except ValueError, inst: print "oops"
128 132 oops
129 133 >>> inserthgheader(['# HG changeset patch'], '# Date ', 'z')
130 134 ['# HG changeset patch', '# Date z']
131 135 >>> inserthgheader(['# HG changeset patch', ''], '# Date ', 'z')
132 136 ['# HG changeset patch', '# Date z', '']
133 137 >>> inserthgheader(['# HG changeset patch', '# User y'], '# Date ', 'z')
134 138 ['# HG changeset patch', '# User y', '# Date z']
135 139 >>> inserthgheader(['# HG changeset patch', '# Date x', '# User y'],
136 140 ... '# User ', 'z')
137 141 ['# HG changeset patch', '# Date x', '# User z']
138 142 >>> inserthgheader(['# HG changeset patch', '# Date y'], '# Date ', 'z')
139 143 ['# HG changeset patch', '# Date z']
140 144 >>> inserthgheader(['# HG changeset patch', '', '# Date y'], '# Date ', 'z')
141 145 ['# HG changeset patch', '# Date z', '', '# Date y']
142 146 >>> inserthgheader(['# HG changeset patch', '# Parent y'], '# Date ', 'z')
143 147 ['# HG changeset patch', '# Date z', '# Parent y']
144 148 """
145 149 start = lines.index('# HG changeset patch') + 1
146 150 newindex = HGHEADERS.index(header)
147 151 bestpos = len(lines)
148 152 for i in range(start, len(lines)):
149 153 line = lines[i]
150 154 if not line.startswith('# '):
151 155 bestpos = min(bestpos, i)
152 156 break
153 157 for lineindex, h in enumerate(HGHEADERS):
154 158 if line.startswith(h):
155 159 if lineindex == newindex:
156 160 lines[i] = header + value
157 161 return lines
158 162 if lineindex > newindex:
159 163 bestpos = min(bestpos, i)
160 164 break # next line
161 165 lines.insert(bestpos, header + value)
162 166 return lines
163 167
164 168 def insertplainheader(lines, header, value):
165 169 """For lines containing a plain patch header, add a header line with value.
166 170 >>> insertplainheader([], 'Date', 'z')
167 171 ['Date: z']
168 172 >>> insertplainheader([''], 'Date', 'z')
169 173 ['Date: z', '']
170 174 >>> insertplainheader(['x'], 'Date', 'z')
171 175 ['Date: z', '', 'x']
172 176 >>> insertplainheader(['From: y', 'x'], 'Date', 'z')
173 177 ['From: y', 'Date: z', '', 'x']
174 178 >>> insertplainheader([' date : x', ' from : y', ''], 'From', 'z')
175 179 [' date : x', 'From: z', '']
176 180 >>> insertplainheader(['', 'Date: y'], 'Date', 'z')
177 181 ['Date: z', '', 'Date: y']
178 182 >>> insertplainheader(['foo: bar', 'DATE: z', 'x'], 'From', 'y')
179 183 ['From: y', 'foo: bar', 'DATE: z', '', 'x']
180 184 """
181 185 newprio = PLAINHEADERS[header.lower()]
182 186 bestpos = len(lines)
183 187 for i, line in enumerate(lines):
184 188 if ':' in line:
185 189 lheader = line.split(':', 1)[0].strip().lower()
186 190 lprio = PLAINHEADERS.get(lheader, newprio + 1)
187 191 if lprio == newprio:
188 192 lines[i] = '%s: %s' % (header, value)
189 193 return lines
190 194 if lprio > newprio and i < bestpos:
191 195 bestpos = i
192 196 else:
193 197 if line:
194 198 lines.insert(i, '')
195 199 if i < bestpos:
196 200 bestpos = i
197 201 break
198 202 lines.insert(bestpos, '%s: %s' % (header, value))
199 203 return lines
200 204
201 205 class patchheader(object):
202 206 def __init__(self, pf, plainmode=False):
203 207 def eatdiff(lines):
204 208 while lines:
205 209 l = lines[-1]
206 210 if (l.startswith("diff -") or
207 211 l.startswith("Index:") or
208 212 l.startswith("===========")):
209 213 del lines[-1]
210 214 else:
211 215 break
212 216 def eatempty(lines):
213 217 while lines:
214 218 if not lines[-1].strip():
215 219 del lines[-1]
216 220 else:
217 221 break
218 222
219 223 message = []
220 224 comments = []
221 225 user = None
222 226 date = None
223 227 parent = None
224 228 format = None
225 229 subject = None
226 230 branch = None
227 231 nodeid = None
228 232 diffstart = 0
229 233
230 234 for line in file(pf):
231 235 line = line.rstrip()
232 236 if (line.startswith('diff --git')
233 237 or (diffstart and line.startswith('+++ '))):
234 238 diffstart = 2
235 239 break
236 240 diffstart = 0 # reset
237 241 if line.startswith("--- "):
238 242 diffstart = 1
239 243 continue
240 244 elif format == "hgpatch":
241 245 # parse values when importing the result of an hg export
242 246 if line.startswith("# User "):
243 247 user = line[7:]
244 248 elif line.startswith("# Date "):
245 249 date = line[7:]
246 250 elif line.startswith("# Parent "):
247 251 parent = line[9:].lstrip() # handle double trailing space
248 252 elif line.startswith("# Branch "):
249 253 branch = line[9:]
250 254 elif line.startswith("# Node ID "):
251 255 nodeid = line[10:]
252 256 elif not line.startswith("# ") and line:
253 257 message.append(line)
254 258 format = None
255 259 elif line == '# HG changeset patch':
256 260 message = []
257 261 format = "hgpatch"
258 262 elif (format != "tagdone" and (line.startswith("Subject: ") or
259 263 line.startswith("subject: "))):
260 264 subject = line[9:]
261 265 format = "tag"
262 266 elif (format != "tagdone" and (line.startswith("From: ") or
263 267 line.startswith("from: "))):
264 268 user = line[6:]
265 269 format = "tag"
266 270 elif (format != "tagdone" and (line.startswith("Date: ") or
267 271 line.startswith("date: "))):
268 272 date = line[6:]
269 273 format = "tag"
270 274 elif format == "tag" and line == "":
271 275 # when looking for tags (subject: from: etc) they
272 276 # end once you find a blank line in the source
273 277 format = "tagdone"
274 278 elif message or line:
275 279 message.append(line)
276 280 comments.append(line)
277 281
278 282 eatdiff(message)
279 283 eatdiff(comments)
280 284 # Remember the exact starting line of the patch diffs before consuming
281 285 # empty lines, for external use by TortoiseHg and others
282 286 self.diffstartline = len(comments)
283 287 eatempty(message)
284 288 eatempty(comments)
285 289
286 290 # make sure message isn't empty
287 291 if format and format.startswith("tag") and subject:
288 292 message.insert(0, subject)
289 293
290 294 self.message = message
291 295 self.comments = comments
292 296 self.user = user
293 297 self.date = date
294 298 self.parent = parent
295 299 # nodeid and branch are for external use by TortoiseHg and others
296 300 self.nodeid = nodeid
297 301 self.branch = branch
298 302 self.haspatch = diffstart > 1
299 303 self.plainmode = (plainmode or
300 304 '# HG changeset patch' not in self.comments and
301 305 any(c.startswith('Date: ') or
302 306 c.startswith('From: ')
303 307 for c in self.comments))
304 308
305 309 def setuser(self, user):
306 310 try:
307 311 inserthgheader(self.comments, '# User ', user)
308 312 except ValueError:
309 313 if self.plainmode:
310 314 insertplainheader(self.comments, 'From', user)
311 315 else:
312 316 tmp = ['# HG changeset patch', '# User ' + user]
313 317 self.comments = tmp + self.comments
314 318 self.user = user
315 319
316 320 def setdate(self, date):
317 321 try:
318 322 inserthgheader(self.comments, '# Date ', date)
319 323 except ValueError:
320 324 if self.plainmode:
321 325 insertplainheader(self.comments, 'Date', date)
322 326 else:
323 327 tmp = ['# HG changeset patch', '# Date ' + date]
324 328 self.comments = tmp + self.comments
325 329 self.date = date
326 330
327 331 def setparent(self, parent):
328 332 try:
329 333 inserthgheader(self.comments, '# Parent ', parent)
330 334 except ValueError:
331 335 if not self.plainmode:
332 336 tmp = ['# HG changeset patch', '# Parent ' + parent]
333 337 self.comments = tmp + self.comments
334 338 self.parent = parent
335 339
336 340 def setmessage(self, message):
337 341 if self.comments:
338 342 self._delmsg()
339 343 self.message = [message]
340 344 if message:
341 345 if self.plainmode and self.comments and self.comments[-1]:
342 346 self.comments.append('')
343 347 self.comments.append(message)
344 348
345 349 def __str__(self):
346 350 s = '\n'.join(self.comments).rstrip()
347 351 if not s:
348 352 return ''
349 353 return s + '\n\n'
350 354
351 355 def _delmsg(self):
352 356 '''Remove existing message, keeping the rest of the comments fields.
353 357 If comments contains 'subject: ', message will prepend
354 358 the field and a blank line.'''
355 359 if self.message:
356 360 subj = 'subject: ' + self.message[0].lower()
357 361 for i in xrange(len(self.comments)):
358 362 if subj == self.comments[i].lower():
359 363 del self.comments[i]
360 364 self.message = self.message[2:]
361 365 break
362 366 ci = 0
363 367 for mi in self.message:
364 368 while mi != self.comments[ci]:
365 369 ci += 1
366 370 del self.comments[ci]
367 371
368 372 def newcommit(repo, phase, *args, **kwargs):
369 373 """helper dedicated to ensure a commit respect mq.secret setting
370 374
371 375 It should be used instead of repo.commit inside the mq source for operation
372 376 creating new changeset.
373 377 """
374 378 repo = repo.unfiltered()
375 379 if phase is None:
376 380 if repo.ui.configbool('mq', 'secret', False):
377 381 phase = phases.secret
378 382 if phase is not None:
379 383 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
380 384 allowemptybackup = repo.ui.backupconfig('ui', 'allowemptycommit')
381 385 try:
382 386 if phase is not None:
383 387 repo.ui.setconfig('phases', 'new-commit', phase, 'mq')
384 388 repo.ui.setconfig('ui', 'allowemptycommit', True)
385 389 return repo.commit(*args, **kwargs)
386 390 finally:
387 391 repo.ui.restoreconfig(allowemptybackup)
388 392 if phase is not None:
389 393 repo.ui.restoreconfig(phasebackup)
390 394
391 395 class AbortNoCleanup(error.Abort):
392 396 pass
393 397
394 398 class queue(object):
395 399 def __init__(self, ui, baseui, path, patchdir=None):
396 400 self.basepath = path
397 401 try:
398 402 fh = open(os.path.join(path, 'patches.queue'))
399 403 cur = fh.read().rstrip()
400 404 fh.close()
401 405 if not cur:
402 406 curpath = os.path.join(path, 'patches')
403 407 else:
404 408 curpath = os.path.join(path, 'patches-' + cur)
405 409 except IOError:
406 410 curpath = os.path.join(path, 'patches')
407 411 self.path = patchdir or curpath
408 412 self.opener = scmutil.opener(self.path)
409 413 self.ui = ui
410 414 self.baseui = baseui
411 415 self.applieddirty = False
412 416 self.seriesdirty = False
413 417 self.added = []
414 418 self.seriespath = "series"
415 419 self.statuspath = "status"
416 420 self.guardspath = "guards"
417 421 self.activeguards = None
418 422 self.guardsdirty = False
419 423 # Handle mq.git as a bool with extended values
420 424 try:
421 425 gitmode = ui.configbool('mq', 'git', None)
422 426 if gitmode is None:
423 427 raise error.ConfigError
424 428 if gitmode:
425 429 self.gitmode = 'yes'
426 430 else:
427 431 self.gitmode = 'no'
428 432 except error.ConfigError:
429 433 self.gitmode = ui.config('mq', 'git', 'auto').lower()
430 434 self.plainmode = ui.configbool('mq', 'plain', False)
431 435 self.checkapplied = True
432 436
433 437 @util.propertycache
434 438 def applied(self):
435 439 def parselines(lines):
436 440 for l in lines:
437 441 entry = l.split(':', 1)
438 442 if len(entry) > 1:
439 443 n, name = entry
440 444 yield statusentry(bin(n), name)
441 445 elif l.strip():
442 446 self.ui.warn(_('malformated mq status line: %s\n') % entry)
443 447 # else we ignore empty lines
444 448 try:
445 449 lines = self.opener.read(self.statuspath).splitlines()
446 450 return list(parselines(lines))
447 451 except IOError, e:
448 452 if e.errno == errno.ENOENT:
449 453 return []
450 454 raise
451 455
452 456 @util.propertycache
453 457 def fullseries(self):
454 458 try:
455 459 return self.opener.read(self.seriespath).splitlines()
456 460 except IOError, e:
457 461 if e.errno == errno.ENOENT:
458 462 return []
459 463 raise
460 464
461 465 @util.propertycache
462 466 def series(self):
463 467 self.parseseries()
464 468 return self.series
465 469
466 470 @util.propertycache
467 471 def seriesguards(self):
468 472 self.parseseries()
469 473 return self.seriesguards
470 474
471 475 def invalidate(self):
472 476 for a in 'applied fullseries series seriesguards'.split():
473 477 if a in self.__dict__:
474 478 delattr(self, a)
475 479 self.applieddirty = False
476 480 self.seriesdirty = False
477 481 self.guardsdirty = False
478 482 self.activeguards = None
479 483
480 484 def diffopts(self, opts={}, patchfn=None):
481 485 diffopts = patchmod.diffopts(self.ui, opts)
482 486 if self.gitmode == 'auto':
483 487 diffopts.upgrade = True
484 488 elif self.gitmode == 'keep':
485 489 pass
486 490 elif self.gitmode in ('yes', 'no'):
487 491 diffopts.git = self.gitmode == 'yes'
488 492 else:
489 493 raise util.Abort(_('mq.git option can be auto/keep/yes/no'
490 494 ' got %s') % self.gitmode)
491 495 if patchfn:
492 496 diffopts = self.patchopts(diffopts, patchfn)
493 497 return diffopts
494 498
495 499 def patchopts(self, diffopts, *patches):
496 500 """Return a copy of input diff options with git set to true if
497 501 referenced patch is a git patch and should be preserved as such.
498 502 """
499 503 diffopts = diffopts.copy()
500 504 if not diffopts.git and self.gitmode == 'keep':
501 505 for patchfn in patches:
502 506 patchf = self.opener(patchfn, 'r')
503 507 # if the patch was a git patch, refresh it as a git patch
504 508 for line in patchf:
505 509 if line.startswith('diff --git'):
506 510 diffopts.git = True
507 511 break
508 512 patchf.close()
509 513 return diffopts
510 514
511 515 def join(self, *p):
512 516 return os.path.join(self.path, *p)
513 517
514 518 def findseries(self, patch):
515 519 def matchpatch(l):
516 520 l = l.split('#', 1)[0]
517 521 return l.strip() == patch
518 522 for index, l in enumerate(self.fullseries):
519 523 if matchpatch(l):
520 524 return index
521 525 return None
522 526
523 527 guard_re = re.compile(r'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
524 528
525 529 def parseseries(self):
526 530 self.series = []
527 531 self.seriesguards = []
528 532 for l in self.fullseries:
529 533 h = l.find('#')
530 534 if h == -1:
531 535 patch = l
532 536 comment = ''
533 537 elif h == 0:
534 538 continue
535 539 else:
536 540 patch = l[:h]
537 541 comment = l[h:]
538 542 patch = patch.strip()
539 543 if patch:
540 544 if patch in self.series:
541 545 raise util.Abort(_('%s appears more than once in %s') %
542 546 (patch, self.join(self.seriespath)))
543 547 self.series.append(patch)
544 548 self.seriesguards.append(self.guard_re.findall(comment))
545 549
546 550 def checkguard(self, guard):
547 551 if not guard:
548 552 return _('guard cannot be an empty string')
549 553 bad_chars = '# \t\r\n\f'
550 554 first = guard[0]
551 555 if first in '-+':
552 556 return (_('guard %r starts with invalid character: %r') %
553 557 (guard, first))
554 558 for c in bad_chars:
555 559 if c in guard:
556 560 return _('invalid character in guard %r: %r') % (guard, c)
557 561
558 562 def setactive(self, guards):
559 563 for guard in guards:
560 564 bad = self.checkguard(guard)
561 565 if bad:
562 566 raise util.Abort(bad)
563 567 guards = sorted(set(guards))
564 568 self.ui.debug('active guards: %s\n' % ' '.join(guards))
565 569 self.activeguards = guards
566 570 self.guardsdirty = True
567 571
568 572 def active(self):
569 573 if self.activeguards is None:
570 574 self.activeguards = []
571 575 try:
572 576 guards = self.opener.read(self.guardspath).split()
573 577 except IOError, err:
574 578 if err.errno != errno.ENOENT:
575 579 raise
576 580 guards = []
577 581 for i, guard in enumerate(guards):
578 582 bad = self.checkguard(guard)
579 583 if bad:
580 584 self.ui.warn('%s:%d: %s\n' %
581 585 (self.join(self.guardspath), i + 1, bad))
582 586 else:
583 587 self.activeguards.append(guard)
584 588 return self.activeguards
585 589
586 590 def setguards(self, idx, guards):
587 591 for g in guards:
588 592 if len(g) < 2:
589 593 raise util.Abort(_('guard %r too short') % g)
590 594 if g[0] not in '-+':
591 595 raise util.Abort(_('guard %r starts with invalid char') % g)
592 596 bad = self.checkguard(g[1:])
593 597 if bad:
594 598 raise util.Abort(bad)
595 599 drop = self.guard_re.sub('', self.fullseries[idx])
596 600 self.fullseries[idx] = drop + ''.join([' #' + g for g in guards])
597 601 self.parseseries()
598 602 self.seriesdirty = True
599 603
600 604 def pushable(self, idx):
601 605 if isinstance(idx, str):
602 606 idx = self.series.index(idx)
603 607 patchguards = self.seriesguards[idx]
604 608 if not patchguards:
605 609 return True, None
606 610 guards = self.active()
607 611 exactneg = [g for g in patchguards if g[0] == '-' and g[1:] in guards]
608 612 if exactneg:
609 613 return False, repr(exactneg[0])
610 614 pos = [g for g in patchguards if g[0] == '+']
611 615 exactpos = [g for g in pos if g[1:] in guards]
612 616 if pos:
613 617 if exactpos:
614 618 return True, repr(exactpos[0])
615 619 return False, ' '.join(map(repr, pos))
616 620 return True, ''
617 621
618 622 def explainpushable(self, idx, all_patches=False):
619 623 if all_patches:
620 624 write = self.ui.write
621 625 else:
622 626 write = self.ui.warn
623 627
624 628 if all_patches or self.ui.verbose:
625 629 if isinstance(idx, str):
626 630 idx = self.series.index(idx)
627 631 pushable, why = self.pushable(idx)
628 632 if all_patches and pushable:
629 633 if why is None:
630 634 write(_('allowing %s - no guards in effect\n') %
631 635 self.series[idx])
632 636 else:
633 637 if not why:
634 638 write(_('allowing %s - no matching negative guards\n') %
635 639 self.series[idx])
636 640 else:
637 641 write(_('allowing %s - guarded by %s\n') %
638 642 (self.series[idx], why))
639 643 if not pushable:
640 644 if why:
641 645 write(_('skipping %s - guarded by %s\n') %
642 646 (self.series[idx], why))
643 647 else:
644 648 write(_('skipping %s - no matching guards\n') %
645 649 self.series[idx])
646 650
647 651 def savedirty(self):
648 652 def writelist(items, path):
649 653 fp = self.opener(path, 'w')
650 654 for i in items:
651 655 fp.write("%s\n" % i)
652 656 fp.close()
653 657 if self.applieddirty:
654 658 writelist(map(str, self.applied), self.statuspath)
655 659 self.applieddirty = False
656 660 if self.seriesdirty:
657 661 writelist(self.fullseries, self.seriespath)
658 662 self.seriesdirty = False
659 663 if self.guardsdirty:
660 664 writelist(self.activeguards, self.guardspath)
661 665 self.guardsdirty = False
662 666 if self.added:
663 667 qrepo = self.qrepo()
664 668 if qrepo:
665 669 qrepo[None].add(f for f in self.added if f not in qrepo[None])
666 670 self.added = []
667 671
668 672 def removeundo(self, repo):
669 673 undo = repo.sjoin('undo')
670 674 if not os.path.exists(undo):
671 675 return
672 676 try:
673 677 os.unlink(undo)
674 678 except OSError, inst:
675 679 self.ui.warn(_('error removing undo: %s\n') % str(inst))
676 680
677 681 def backup(self, repo, files, copy=False):
678 682 # backup local changes in --force case
679 683 for f in sorted(files):
680 684 absf = repo.wjoin(f)
681 685 if os.path.lexists(absf):
682 686 self.ui.note(_('saving current version of %s as %s\n') %
683 687 (f, f + '.orig'))
684 688 if copy:
685 689 util.copyfile(absf, absf + '.orig')
686 690 else:
687 691 util.rename(absf, absf + '.orig')
688 692
689 693 def printdiff(self, repo, diffopts, node1, node2=None, files=None,
690 694 fp=None, changes=None, opts={}):
691 695 stat = opts.get('stat')
692 696 m = scmutil.match(repo[node1], files, opts)
693 697 cmdutil.diffordiffstat(self.ui, repo, diffopts, node1, node2, m,
694 698 changes, stat, fp)
695 699
696 700 def mergeone(self, repo, mergeq, head, patch, rev, diffopts):
697 701 # first try just applying the patch
698 702 (err, n) = self.apply(repo, [patch], update_status=False,
699 703 strict=True, merge=rev)
700 704
701 705 if err == 0:
702 706 return (err, n)
703 707
704 708 if n is None:
705 709 raise util.Abort(_("apply failed for patch %s") % patch)
706 710
707 711 self.ui.warn(_("patch didn't work out, merging %s\n") % patch)
708 712
709 713 # apply failed, strip away that rev and merge.
710 714 hg.clean(repo, head)
711 715 strip(self.ui, repo, [n], update=False, backup=False)
712 716
713 717 ctx = repo[rev]
714 718 ret = hg.merge(repo, rev)
715 719 if ret:
716 720 raise util.Abort(_("update returned %d") % ret)
717 721 n = newcommit(repo, None, ctx.description(), ctx.user(), force=True)
718 722 if n is None:
719 723 raise util.Abort(_("repo commit failed"))
720 724 try:
721 725 ph = patchheader(mergeq.join(patch), self.plainmode)
722 726 except Exception:
723 727 raise util.Abort(_("unable to read %s") % patch)
724 728
725 729 diffopts = self.patchopts(diffopts, patch)
726 730 patchf = self.opener(patch, "w")
727 731 comments = str(ph)
728 732 if comments:
729 733 patchf.write(comments)
730 734 self.printdiff(repo, diffopts, head, n, fp=patchf)
731 735 patchf.close()
732 736 self.removeundo(repo)
733 737 return (0, n)
734 738
735 739 def qparents(self, repo, rev=None):
736 740 """return the mq handled parent or p1
737 741
738 742 In some case where mq get himself in being the parent of a merge the
739 743 appropriate parent may be p2.
740 744 (eg: an in progress merge started with mq disabled)
741 745
742 746 If no parent are managed by mq, p1 is returned.
743 747 """
744 748 if rev is None:
745 749 (p1, p2) = repo.dirstate.parents()
746 750 if p2 == nullid:
747 751 return p1
748 752 if not self.applied:
749 753 return None
750 754 return self.applied[-1].node
751 755 p1, p2 = repo.changelog.parents(rev)
752 756 if p2 != nullid and p2 in [x.node for x in self.applied]:
753 757 return p2
754 758 return p1
755 759
756 760 def mergepatch(self, repo, mergeq, series, diffopts):
757 761 if not self.applied:
758 762 # each of the patches merged in will have two parents. This
759 763 # can confuse the qrefresh, qdiff, and strip code because it
760 764 # needs to know which parent is actually in the patch queue.
761 765 # so, we insert a merge marker with only one parent. This way
762 766 # the first patch in the queue is never a merge patch
763 767 #
764 768 pname = ".hg.patches.merge.marker"
765 769 n = newcommit(repo, None, '[mq]: merge marker', force=True)
766 770 self.removeundo(repo)
767 771 self.applied.append(statusentry(n, pname))
768 772 self.applieddirty = True
769 773
770 774 head = self.qparents(repo)
771 775
772 776 for patch in series:
773 777 patch = mergeq.lookup(patch, strict=True)
774 778 if not patch:
775 779 self.ui.warn(_("patch %s does not exist\n") % patch)
776 780 return (1, None)
777 781 pushable, reason = self.pushable(patch)
778 782 if not pushable:
779 783 self.explainpushable(patch, all_patches=True)
780 784 continue
781 785 info = mergeq.isapplied(patch)
782 786 if not info:
783 787 self.ui.warn(_("patch %s is not applied\n") % patch)
784 788 return (1, None)
785 789 rev = info[1]
786 790 err, head = self.mergeone(repo, mergeq, head, patch, rev, diffopts)
787 791 if head:
788 792 self.applied.append(statusentry(head, patch))
789 793 self.applieddirty = True
790 794 if err:
791 795 return (err, head)
792 796 self.savedirty()
793 797 return (0, head)
794 798
795 799 def patch(self, repo, patchfile):
796 800 '''Apply patchfile to the working directory.
797 801 patchfile: name of patch file'''
798 802 files = set()
799 803 try:
800 804 fuzz = patchmod.patch(self.ui, repo, patchfile, strip=1,
801 805 files=files, eolmode=None)
802 806 return (True, list(files), fuzz)
803 807 except Exception, inst:
804 808 self.ui.note(str(inst) + '\n')
805 809 if not self.ui.verbose:
806 810 self.ui.warn(_("patch failed, unable to continue (try -v)\n"))
807 811 self.ui.traceback()
808 812 return (False, list(files), False)
809 813
810 814 def apply(self, repo, series, list=False, update_status=True,
811 815 strict=False, patchdir=None, merge=None, all_files=None,
812 816 tobackup=None, keepchanges=False):
813 817 wlock = dsguard = lock = tr = None
814 818 try:
815 819 wlock = repo.wlock()
816 820 dsguard = cmdutil.dirstateguard(repo, 'mq.apply')
817 821 lock = repo.lock()
818 822 tr = repo.transaction("qpush")
819 823 try:
820 824 ret = self._apply(repo, series, list, update_status,
821 825 strict, patchdir, merge, all_files=all_files,
822 826 tobackup=tobackup, keepchanges=keepchanges)
823 827 tr.close()
824 828 self.savedirty()
825 829 dsguard.close()
826 830 return ret
827 831 except AbortNoCleanup:
828 832 tr.close()
829 833 self.savedirty()
830 834 dsguard.close()
831 835 raise
832 836 except: # re-raises
833 837 try:
834 838 tr.abort()
835 839 finally:
836 840 repo.invalidate()
837 841 self.invalidate()
838 842 raise
839 843 finally:
840 844 release(tr, lock, dsguard, wlock)
841 845 self.removeundo(repo)
842 846
843 847 def _apply(self, repo, series, list=False, update_status=True,
844 848 strict=False, patchdir=None, merge=None, all_files=None,
845 849 tobackup=None, keepchanges=False):
846 850 """returns (error, hash)
847 851
848 852 error = 1 for unable to read, 2 for patch failed, 3 for patch
849 853 fuzz. tobackup is None or a set of files to backup before they
850 854 are modified by a patch.
851 855 """
852 856 # TODO unify with commands.py
853 857 if not patchdir:
854 858 patchdir = self.path
855 859 err = 0
856 860 n = None
857 861 for patchname in series:
858 862 pushable, reason = self.pushable(patchname)
859 863 if not pushable:
860 864 self.explainpushable(patchname, all_patches=True)
861 865 continue
862 866 self.ui.status(_("applying %s\n") % patchname)
863 867 pf = os.path.join(patchdir, patchname)
864 868
865 869 try:
866 870 ph = patchheader(self.join(patchname), self.plainmode)
867 871 except IOError:
868 872 self.ui.warn(_("unable to read %s\n") % patchname)
869 873 err = 1
870 874 break
871 875
872 876 message = ph.message
873 877 if not message:
874 878 # The commit message should not be translated
875 879 message = "imported patch %s\n" % patchname
876 880 else:
877 881 if list:
878 882 # The commit message should not be translated
879 883 message.append("\nimported patch %s" % patchname)
880 884 message = '\n'.join(message)
881 885
882 886 if ph.haspatch:
883 887 if tobackup:
884 888 touched = patchmod.changedfiles(self.ui, repo, pf)
885 889 touched = set(touched) & tobackup
886 890 if touched and keepchanges:
887 891 raise AbortNoCleanup(
888 892 _("conflicting local changes found"),
889 893 hint=_("did you forget to qrefresh?"))
890 894 self.backup(repo, touched, copy=True)
891 895 tobackup = tobackup - touched
892 896 (patcherr, files, fuzz) = self.patch(repo, pf)
893 897 if all_files is not None:
894 898 all_files.update(files)
895 899 patcherr = not patcherr
896 900 else:
897 901 self.ui.warn(_("patch %s is empty\n") % patchname)
898 902 patcherr, files, fuzz = 0, [], 0
899 903
900 904 if merge and files:
901 905 # Mark as removed/merged and update dirstate parent info
902 906 removed = []
903 907 merged = []
904 908 for f in files:
905 909 if os.path.lexists(repo.wjoin(f)):
906 910 merged.append(f)
907 911 else:
908 912 removed.append(f)
909 913 repo.dirstate.beginparentchange()
910 914 for f in removed:
911 915 repo.dirstate.remove(f)
912 916 for f in merged:
913 917 repo.dirstate.merge(f)
914 918 p1, p2 = repo.dirstate.parents()
915 919 repo.setparents(p1, merge)
916 920 repo.dirstate.endparentchange()
917 921
918 922 if all_files and '.hgsubstate' in all_files:
919 923 wctx = repo[None]
920 924 pctx = repo['.']
921 925 overwrite = False
922 926 mergedsubstate = subrepo.submerge(repo, pctx, wctx, wctx,
923 927 overwrite)
924 928 files += mergedsubstate.keys()
925 929
926 930 match = scmutil.matchfiles(repo, files or [])
927 931 oldtip = repo['tip']
928 932 n = newcommit(repo, None, message, ph.user, ph.date, match=match,
929 933 force=True)
930 934 if repo['tip'] == oldtip:
931 935 raise util.Abort(_("qpush exactly duplicates child changeset"))
932 936 if n is None:
933 937 raise util.Abort(_("repository commit failed"))
934 938
935 939 if update_status:
936 940 self.applied.append(statusentry(n, patchname))
937 941
938 942 if patcherr:
939 943 self.ui.warn(_("patch failed, rejects left in working "
940 944 "directory\n"))
941 945 err = 2
942 946 break
943 947
944 948 if fuzz and strict:
945 949 self.ui.warn(_("fuzz found when applying patch, stopping\n"))
946 950 err = 3
947 951 break
948 952 return (err, n)
949 953
950 954 def _cleanup(self, patches, numrevs, keep=False):
951 955 if not keep:
952 956 r = self.qrepo()
953 957 if r:
954 958 r[None].forget(patches)
955 959 for p in patches:
956 960 try:
957 961 os.unlink(self.join(p))
958 962 except OSError, inst:
959 963 if inst.errno != errno.ENOENT:
960 964 raise
961 965
962 966 qfinished = []
963 967 if numrevs:
964 968 qfinished = self.applied[:numrevs]
965 969 del self.applied[:numrevs]
966 970 self.applieddirty = True
967 971
968 972 unknown = []
969 973
970 974 for (i, p) in sorted([(self.findseries(p), p) for p in patches],
971 975 reverse=True):
972 976 if i is not None:
973 977 del self.fullseries[i]
974 978 else:
975 979 unknown.append(p)
976 980
977 981 if unknown:
978 982 if numrevs:
979 983 rev = dict((entry.name, entry.node) for entry in qfinished)
980 984 for p in unknown:
981 985 msg = _('revision %s refers to unknown patches: %s\n')
982 986 self.ui.warn(msg % (short(rev[p]), p))
983 987 else:
984 988 msg = _('unknown patches: %s\n')
985 989 raise util.Abort(''.join(msg % p for p in unknown))
986 990
987 991 self.parseseries()
988 992 self.seriesdirty = True
989 993 return [entry.node for entry in qfinished]
990 994
991 995 def _revpatches(self, repo, revs):
992 996 firstrev = repo[self.applied[0].node].rev()
993 997 patches = []
994 998 for i, rev in enumerate(revs):
995 999
996 1000 if rev < firstrev:
997 1001 raise util.Abort(_('revision %d is not managed') % rev)
998 1002
999 1003 ctx = repo[rev]
1000 1004 base = self.applied[i].node
1001 1005 if ctx.node() != base:
1002 1006 msg = _('cannot delete revision %d above applied patches')
1003 1007 raise util.Abort(msg % rev)
1004 1008
1005 1009 patch = self.applied[i].name
1006 1010 for fmt in ('[mq]: %s', 'imported patch %s'):
1007 1011 if ctx.description() == fmt % patch:
1008 1012 msg = _('patch %s finalized without changeset message\n')
1009 1013 repo.ui.status(msg % patch)
1010 1014 break
1011 1015
1012 1016 patches.append(patch)
1013 1017 return patches
1014 1018
1015 1019 def finish(self, repo, revs):
1016 1020 # Manually trigger phase computation to ensure phasedefaults is
1017 1021 # executed before we remove the patches.
1018 1022 repo._phasecache
1019 1023 patches = self._revpatches(repo, sorted(revs))
1020 1024 qfinished = self._cleanup(patches, len(patches))
1021 1025 if qfinished and repo.ui.configbool('mq', 'secret', False):
1022 1026 # only use this logic when the secret option is added
1023 1027 oldqbase = repo[qfinished[0]]
1024 1028 tphase = repo.ui.config('phases', 'new-commit', phases.draft)
1025 1029 if oldqbase.phase() > tphase and oldqbase.p1().phase() <= tphase:
1026 1030 tr = repo.transaction('qfinish')
1027 1031 try:
1028 1032 phases.advanceboundary(repo, tr, tphase, qfinished)
1029 1033 tr.close()
1030 1034 finally:
1031 1035 tr.release()
1032 1036
1033 1037 def delete(self, repo, patches, opts):
1034 1038 if not patches and not opts.get('rev'):
1035 1039 raise util.Abort(_('qdelete requires at least one revision or '
1036 1040 'patch name'))
1037 1041
1038 1042 realpatches = []
1039 1043 for patch in patches:
1040 1044 patch = self.lookup(patch, strict=True)
1041 1045 info = self.isapplied(patch)
1042 1046 if info:
1043 1047 raise util.Abort(_("cannot delete applied patch %s") % patch)
1044 1048 if patch not in self.series:
1045 1049 raise util.Abort(_("patch %s not in series file") % patch)
1046 1050 if patch not in realpatches:
1047 1051 realpatches.append(patch)
1048 1052
1049 1053 numrevs = 0
1050 1054 if opts.get('rev'):
1051 1055 if not self.applied:
1052 1056 raise util.Abort(_('no patches applied'))
1053 1057 revs = scmutil.revrange(repo, opts.get('rev'))
1054 1058 revs.sort()
1055 1059 revpatches = self._revpatches(repo, revs)
1056 1060 realpatches += revpatches
1057 1061 numrevs = len(revpatches)
1058 1062
1059 1063 self._cleanup(realpatches, numrevs, opts.get('keep'))
1060 1064
1061 1065 def checktoppatch(self, repo):
1062 1066 '''check that working directory is at qtip'''
1063 1067 if self.applied:
1064 1068 top = self.applied[-1].node
1065 1069 patch = self.applied[-1].name
1066 1070 if repo.dirstate.p1() != top:
1067 1071 raise util.Abort(_("working directory revision is not qtip"))
1068 1072 return top, patch
1069 1073 return None, None
1070 1074
1071 1075 def putsubstate2changes(self, substatestate, changes):
1072 1076 for files in changes[:3]:
1073 1077 if '.hgsubstate' in files:
1074 1078 return # already listed up
1075 1079 # not yet listed up
1076 1080 if substatestate in 'a?':
1077 1081 changes[1].append('.hgsubstate')
1078 1082 elif substatestate in 'r':
1079 1083 changes[2].append('.hgsubstate')
1080 1084 else: # modified
1081 1085 changes[0].append('.hgsubstate')
1082 1086
1083 1087 def checklocalchanges(self, repo, force=False, refresh=True):
1084 1088 excsuffix = ''
1085 1089 if refresh:
1086 1090 excsuffix = ', refresh first'
1087 1091 # plain versions for i18n tool to detect them
1088 1092 _("local changes found, refresh first")
1089 1093 _("local changed subrepos found, refresh first")
1090 1094 return checklocalchanges(repo, force, excsuffix)
1091 1095
1092 1096 _reserved = ('series', 'status', 'guards', '.', '..')
1093 1097 def checkreservedname(self, name):
1094 1098 if name in self._reserved:
1095 1099 raise util.Abort(_('"%s" cannot be used as the name of a patch')
1096 1100 % name)
1097 1101 for prefix in ('.hg', '.mq'):
1098 1102 if name.startswith(prefix):
1099 1103 raise util.Abort(_('patch name cannot begin with "%s"')
1100 1104 % prefix)
1101 1105 for c in ('#', ':'):
1102 1106 if c in name:
1103 1107 raise util.Abort(_('"%s" cannot be used in the name of a patch')
1104 1108 % c)
1105 1109
1106 1110 def checkpatchname(self, name, force=False):
1107 1111 self.checkreservedname(name)
1108 1112 if not force and os.path.exists(self.join(name)):
1109 1113 if os.path.isdir(self.join(name)):
1110 1114 raise util.Abort(_('"%s" already exists as a directory')
1111 1115 % name)
1112 1116 else:
1113 1117 raise util.Abort(_('patch "%s" already exists') % name)
1114 1118
1115 1119 def checkkeepchanges(self, keepchanges, force):
1116 1120 if force and keepchanges:
1117 1121 raise util.Abort(_('cannot use both --force and --keep-changes'))
1118 1122
1119 1123 def new(self, repo, patchfn, *pats, **opts):
1120 1124 """options:
1121 1125 msg: a string or a no-argument function returning a string
1122 1126 """
1123 1127 msg = opts.get('msg')
1124 1128 edit = opts.get('edit')
1125 1129 editform = opts.get('editform', 'mq.qnew')
1126 1130 user = opts.get('user')
1127 1131 date = opts.get('date')
1128 1132 if date:
1129 1133 date = util.parsedate(date)
1130 1134 diffopts = self.diffopts({'git': opts.get('git')})
1131 1135 if opts.get('checkname', True):
1132 1136 self.checkpatchname(patchfn)
1133 1137 inclsubs = checksubstate(repo)
1134 1138 if inclsubs:
1135 1139 substatestate = repo.dirstate['.hgsubstate']
1136 1140 if opts.get('include') or opts.get('exclude') or pats:
1137 1141 match = scmutil.match(repo[None], pats, opts)
1138 1142 # detect missing files in pats
1139 1143 def badfn(f, msg):
1140 1144 if f != '.hgsubstate': # .hgsubstate is auto-created
1141 1145 raise util.Abort('%s: %s' % (f, msg))
1142 1146 match.bad = badfn
1143 1147 changes = repo.status(match=match)
1144 1148 else:
1145 1149 changes = self.checklocalchanges(repo, force=True)
1146 1150 commitfiles = list(inclsubs)
1147 1151 for files in changes[:3]:
1148 1152 commitfiles.extend(files)
1149 1153 match = scmutil.matchfiles(repo, commitfiles)
1150 1154 if len(repo[None].parents()) > 1:
1151 1155 raise util.Abort(_('cannot manage merge changesets'))
1152 1156 self.checktoppatch(repo)
1153 1157 insert = self.fullseriesend()
1154 1158 wlock = repo.wlock()
1155 1159 try:
1156 1160 try:
1157 1161 # if patch file write fails, abort early
1158 1162 p = self.opener(patchfn, "w")
1159 1163 except IOError, e:
1160 1164 raise util.Abort(_('cannot write patch "%s": %s')
1161 1165 % (patchfn, e.strerror))
1162 1166 try:
1163 1167 defaultmsg = "[mq]: %s" % patchfn
1164 1168 editor = cmdutil.getcommiteditor(editform=editform)
1165 1169 if edit:
1166 1170 def finishdesc(desc):
1167 1171 if desc.rstrip():
1168 1172 return desc
1169 1173 else:
1170 1174 return defaultmsg
1171 1175 # i18n: this message is shown in editor with "HG: " prefix
1172 1176 extramsg = _('Leave message empty to use default message.')
1173 1177 editor = cmdutil.getcommiteditor(finishdesc=finishdesc,
1174 1178 extramsg=extramsg,
1175 1179 editform=editform)
1176 1180 commitmsg = msg
1177 1181 else:
1178 1182 commitmsg = msg or defaultmsg
1179 1183
1180 1184 n = newcommit(repo, None, commitmsg, user, date, match=match,
1181 1185 force=True, editor=editor)
1182 1186 if n is None:
1183 1187 raise util.Abort(_("repo commit failed"))
1184 1188 try:
1185 1189 self.fullseries[insert:insert] = [patchfn]
1186 1190 self.applied.append(statusentry(n, patchfn))
1187 1191 self.parseseries()
1188 1192 self.seriesdirty = True
1189 1193 self.applieddirty = True
1190 1194 nctx = repo[n]
1191 1195 ph = patchheader(self.join(patchfn), self.plainmode)
1192 1196 if user:
1193 1197 ph.setuser(user)
1194 1198 if date:
1195 1199 ph.setdate('%s %s' % date)
1196 1200 ph.setparent(hex(nctx.p1().node()))
1197 1201 msg = nctx.description().strip()
1198 1202 if msg == defaultmsg.strip():
1199 1203 msg = ''
1200 1204 ph.setmessage(msg)
1201 1205 p.write(str(ph))
1202 1206 if commitfiles:
1203 1207 parent = self.qparents(repo, n)
1204 1208 if inclsubs:
1205 1209 self.putsubstate2changes(substatestate, changes)
1206 1210 chunks = patchmod.diff(repo, node1=parent, node2=n,
1207 1211 changes=changes, opts=diffopts)
1208 1212 for chunk in chunks:
1209 1213 p.write(chunk)
1210 1214 p.close()
1211 1215 r = self.qrepo()
1212 1216 if r:
1213 1217 r[None].add([patchfn])
1214 1218 except: # re-raises
1215 1219 repo.rollback()
1216 1220 raise
1217 1221 except Exception:
1218 1222 patchpath = self.join(patchfn)
1219 1223 try:
1220 1224 os.unlink(patchpath)
1221 1225 except OSError:
1222 1226 self.ui.warn(_('error unlinking %s\n') % patchpath)
1223 1227 raise
1224 1228 self.removeundo(repo)
1225 1229 finally:
1226 1230 release(wlock)
1227 1231
1228 1232 def isapplied(self, patch):
1229 1233 """returns (index, rev, patch)"""
1230 1234 for i, a in enumerate(self.applied):
1231 1235 if a.name == patch:
1232 1236 return (i, a.node, a.name)
1233 1237 return None
1234 1238
1235 1239 # if the exact patch name does not exist, we try a few
1236 1240 # variations. If strict is passed, we try only #1
1237 1241 #
1238 1242 # 1) a number (as string) to indicate an offset in the series file
1239 1243 # 2) a unique substring of the patch name was given
1240 1244 # 3) patchname[-+]num to indicate an offset in the series file
1241 1245 def lookup(self, patch, strict=False):
1242 1246 def partialname(s):
1243 1247 if s in self.series:
1244 1248 return s
1245 1249 matches = [x for x in self.series if s in x]
1246 1250 if len(matches) > 1:
1247 1251 self.ui.warn(_('patch name "%s" is ambiguous:\n') % s)
1248 1252 for m in matches:
1249 1253 self.ui.warn(' %s\n' % m)
1250 1254 return None
1251 1255 if matches:
1252 1256 return matches[0]
1253 1257 if self.series and self.applied:
1254 1258 if s == 'qtip':
1255 1259 return self.series[self.seriesend(True) - 1]
1256 1260 if s == 'qbase':
1257 1261 return self.series[0]
1258 1262 return None
1259 1263
1260 1264 if patch in self.series:
1261 1265 return patch
1262 1266
1263 1267 if not os.path.isfile(self.join(patch)):
1264 1268 try:
1265 1269 sno = int(patch)
1266 1270 except (ValueError, OverflowError):
1267 1271 pass
1268 1272 else:
1269 1273 if -len(self.series) <= sno < len(self.series):
1270 1274 return self.series[sno]
1271 1275
1272 1276 if not strict:
1273 1277 res = partialname(patch)
1274 1278 if res:
1275 1279 return res
1276 1280 minus = patch.rfind('-')
1277 1281 if minus >= 0:
1278 1282 res = partialname(patch[:minus])
1279 1283 if res:
1280 1284 i = self.series.index(res)
1281 1285 try:
1282 1286 off = int(patch[minus + 1:] or 1)
1283 1287 except (ValueError, OverflowError):
1284 1288 pass
1285 1289 else:
1286 1290 if i - off >= 0:
1287 1291 return self.series[i - off]
1288 1292 plus = patch.rfind('+')
1289 1293 if plus >= 0:
1290 1294 res = partialname(patch[:plus])
1291 1295 if res:
1292 1296 i = self.series.index(res)
1293 1297 try:
1294 1298 off = int(patch[plus + 1:] or 1)
1295 1299 except (ValueError, OverflowError):
1296 1300 pass
1297 1301 else:
1298 1302 if i + off < len(self.series):
1299 1303 return self.series[i + off]
1300 1304 raise util.Abort(_("patch %s not in series") % patch)
1301 1305
1302 1306 def push(self, repo, patch=None, force=False, list=False, mergeq=None,
1303 1307 all=False, move=False, exact=False, nobackup=False,
1304 1308 keepchanges=False):
1305 1309 self.checkkeepchanges(keepchanges, force)
1306 1310 diffopts = self.diffopts()
1307 1311 wlock = repo.wlock()
1308 1312 try:
1309 1313 heads = []
1310 1314 for hs in repo.branchmap().itervalues():
1311 1315 heads.extend(hs)
1312 1316 if not heads:
1313 1317 heads = [nullid]
1314 1318 if repo.dirstate.p1() not in heads and not exact:
1315 1319 self.ui.status(_("(working directory not at a head)\n"))
1316 1320
1317 1321 if not self.series:
1318 1322 self.ui.warn(_('no patches in series\n'))
1319 1323 return 0
1320 1324
1321 1325 # Suppose our series file is: A B C and the current 'top'
1322 1326 # patch is B. qpush C should be performed (moving forward)
1323 1327 # qpush B is a NOP (no change) qpush A is an error (can't
1324 1328 # go backwards with qpush)
1325 1329 if patch:
1326 1330 patch = self.lookup(patch)
1327 1331 info = self.isapplied(patch)
1328 1332 if info and info[0] >= len(self.applied) - 1:
1329 1333 self.ui.warn(
1330 1334 _('qpush: %s is already at the top\n') % patch)
1331 1335 return 0
1332 1336
1333 1337 pushable, reason = self.pushable(patch)
1334 1338 if pushable:
1335 1339 if self.series.index(patch) < self.seriesend():
1336 1340 raise util.Abort(
1337 1341 _("cannot push to a previous patch: %s") % patch)
1338 1342 else:
1339 1343 if reason:
1340 1344 reason = _('guarded by %s') % reason
1341 1345 else:
1342 1346 reason = _('no matching guards')
1343 1347 self.ui.warn(_("cannot push '%s' - %s\n") % (patch, reason))
1344 1348 return 1
1345 1349 elif all:
1346 1350 patch = self.series[-1]
1347 1351 if self.isapplied(patch):
1348 1352 self.ui.warn(_('all patches are currently applied\n'))
1349 1353 return 0
1350 1354
1351 1355 # Following the above example, starting at 'top' of B:
1352 1356 # qpush should be performed (pushes C), but a subsequent
1353 1357 # qpush without an argument is an error (nothing to
1354 1358 # apply). This allows a loop of "...while hg qpush..." to
1355 1359 # work as it detects an error when done
1356 1360 start = self.seriesend()
1357 1361 if start == len(self.series):
1358 1362 self.ui.warn(_('patch series already fully applied\n'))
1359 1363 return 1
1360 1364 if not force and not keepchanges:
1361 1365 self.checklocalchanges(repo, refresh=self.applied)
1362 1366
1363 1367 if exact:
1364 1368 if keepchanges:
1365 1369 raise util.Abort(
1366 1370 _("cannot use --exact and --keep-changes together"))
1367 1371 if move:
1368 1372 raise util.Abort(_('cannot use --exact and --move '
1369 1373 'together'))
1370 1374 if self.applied:
1371 1375 raise util.Abort(_('cannot push --exact with applied '
1372 1376 'patches'))
1373 1377 root = self.series[start]
1374 1378 target = patchheader(self.join(root), self.plainmode).parent
1375 1379 if not target:
1376 1380 raise util.Abort(
1377 1381 _("%s does not have a parent recorded") % root)
1378 1382 if not repo[target] == repo['.']:
1379 1383 hg.update(repo, target)
1380 1384
1381 1385 if move:
1382 1386 if not patch:
1383 1387 raise util.Abort(_("please specify the patch to move"))
1384 1388 for fullstart, rpn in enumerate(self.fullseries):
1385 1389 # strip markers for patch guards
1386 1390 if self.guard_re.split(rpn, 1)[0] == self.series[start]:
1387 1391 break
1388 1392 for i, rpn in enumerate(self.fullseries[fullstart:]):
1389 1393 # strip markers for patch guards
1390 1394 if self.guard_re.split(rpn, 1)[0] == patch:
1391 1395 break
1392 1396 index = fullstart + i
1393 1397 assert index < len(self.fullseries)
1394 1398 fullpatch = self.fullseries[index]
1395 1399 del self.fullseries[index]
1396 1400 self.fullseries.insert(fullstart, fullpatch)
1397 1401 self.parseseries()
1398 1402 self.seriesdirty = True
1399 1403
1400 1404 self.applieddirty = True
1401 1405 if start > 0:
1402 1406 self.checktoppatch(repo)
1403 1407 if not patch:
1404 1408 patch = self.series[start]
1405 1409 end = start + 1
1406 1410 else:
1407 1411 end = self.series.index(patch, start) + 1
1408 1412
1409 1413 tobackup = set()
1410 1414 if (not nobackup and force) or keepchanges:
1411 1415 status = self.checklocalchanges(repo, force=True)
1412 1416 if keepchanges:
1413 1417 tobackup.update(status.modified + status.added +
1414 1418 status.removed + status.deleted)
1415 1419 else:
1416 1420 tobackup.update(status.modified + status.added)
1417 1421
1418 1422 s = self.series[start:end]
1419 1423 all_files = set()
1420 1424 try:
1421 1425 if mergeq:
1422 1426 ret = self.mergepatch(repo, mergeq, s, diffopts)
1423 1427 else:
1424 1428 ret = self.apply(repo, s, list, all_files=all_files,
1425 1429 tobackup=tobackup, keepchanges=keepchanges)
1426 1430 except AbortNoCleanup:
1427 1431 raise
1428 1432 except: # re-raises
1429 1433 self.ui.warn(_('cleaning up working directory...'))
1430 1434 node = repo.dirstate.p1()
1431 1435 hg.revert(repo, node, None)
1432 1436 # only remove unknown files that we know we touched or
1433 1437 # created while patching
1434 1438 for f in all_files:
1435 1439 if f not in repo.dirstate:
1436 1440 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
1437 1441 self.ui.warn(_('done\n'))
1438 1442 raise
1439 1443
1440 1444 if not self.applied:
1441 1445 return ret[0]
1442 1446 top = self.applied[-1].name
1443 1447 if ret[0] and ret[0] > 1:
1444 1448 msg = _("errors during apply, please fix and refresh %s\n")
1445 1449 self.ui.write(msg % top)
1446 1450 else:
1447 1451 self.ui.write(_("now at: %s\n") % top)
1448 1452 return ret[0]
1449 1453
1450 1454 finally:
1451 1455 wlock.release()
1452 1456
1453 1457 def pop(self, repo, patch=None, force=False, update=True, all=False,
1454 1458 nobackup=False, keepchanges=False):
1455 1459 self.checkkeepchanges(keepchanges, force)
1456 1460 wlock = repo.wlock()
1457 1461 try:
1458 1462 if patch:
1459 1463 # index, rev, patch
1460 1464 info = self.isapplied(patch)
1461 1465 if not info:
1462 1466 patch = self.lookup(patch)
1463 1467 info = self.isapplied(patch)
1464 1468 if not info:
1465 1469 raise util.Abort(_("patch %s is not applied") % patch)
1466 1470
1467 1471 if not self.applied:
1468 1472 # Allow qpop -a to work repeatedly,
1469 1473 # but not qpop without an argument
1470 1474 self.ui.warn(_("no patches applied\n"))
1471 1475 return not all
1472 1476
1473 1477 if all:
1474 1478 start = 0
1475 1479 elif patch:
1476 1480 start = info[0] + 1
1477 1481 else:
1478 1482 start = len(self.applied) - 1
1479 1483
1480 1484 if start >= len(self.applied):
1481 1485 self.ui.warn(_("qpop: %s is already at the top\n") % patch)
1482 1486 return
1483 1487
1484 1488 if not update:
1485 1489 parents = repo.dirstate.parents()
1486 1490 rr = [x.node for x in self.applied]
1487 1491 for p in parents:
1488 1492 if p in rr:
1489 1493 self.ui.warn(_("qpop: forcing dirstate update\n"))
1490 1494 update = True
1491 1495 else:
1492 1496 parents = [p.node() for p in repo[None].parents()]
1493 1497 needupdate = False
1494 1498 for entry in self.applied[start:]:
1495 1499 if entry.node in parents:
1496 1500 needupdate = True
1497 1501 break
1498 1502 update = needupdate
1499 1503
1500 1504 tobackup = set()
1501 1505 if update:
1502 1506 s = self.checklocalchanges(repo, force=force or keepchanges)
1503 1507 if force:
1504 1508 if not nobackup:
1505 1509 tobackup.update(s.modified + s.added)
1506 1510 elif keepchanges:
1507 1511 tobackup.update(s.modified + s.added +
1508 1512 s.removed + s.deleted)
1509 1513
1510 1514 self.applieddirty = True
1511 1515 end = len(self.applied)
1512 1516 rev = self.applied[start].node
1513 1517
1514 1518 try:
1515 1519 heads = repo.changelog.heads(rev)
1516 1520 except error.LookupError:
1517 1521 node = short(rev)
1518 1522 raise util.Abort(_('trying to pop unknown node %s') % node)
1519 1523
1520 1524 if heads != [self.applied[-1].node]:
1521 1525 raise util.Abort(_("popping would remove a revision not "
1522 1526 "managed by this patch queue"))
1523 1527 if not repo[self.applied[-1].node].mutable():
1524 1528 raise util.Abort(
1525 1529 _("popping would remove an immutable revision"),
1526 1530 hint=_('see "hg help phases" for details'))
1527 1531
1528 1532 # we know there are no local changes, so we can make a simplified
1529 1533 # form of hg.update.
1530 1534 if update:
1531 1535 qp = self.qparents(repo, rev)
1532 1536 ctx = repo[qp]
1533 1537 m, a, r, d = repo.status(qp, '.')[:4]
1534 1538 if d:
1535 1539 raise util.Abort(_("deletions found between repo revs"))
1536 1540
1537 1541 tobackup = set(a + m + r) & tobackup
1538 1542 if keepchanges and tobackup:
1539 1543 raise util.Abort(_("local changes found, refresh first"))
1540 1544 self.backup(repo, tobackup)
1541 1545 repo.dirstate.beginparentchange()
1542 1546 for f in a:
1543 1547 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
1544 1548 repo.dirstate.drop(f)
1545 1549 for f in m + r:
1546 1550 fctx = ctx[f]
1547 1551 repo.wwrite(f, fctx.data(), fctx.flags())
1548 1552 repo.dirstate.normal(f)
1549 1553 repo.setparents(qp, nullid)
1550 1554 repo.dirstate.endparentchange()
1551 1555 for patch in reversed(self.applied[start:end]):
1552 1556 self.ui.status(_("popping %s\n") % patch.name)
1553 1557 del self.applied[start:end]
1554 1558 strip(self.ui, repo, [rev], update=False, backup=False)
1555 1559 for s, state in repo['.'].substate.items():
1556 1560 repo['.'].sub(s).get(state)
1557 1561 if self.applied:
1558 1562 self.ui.write(_("now at: %s\n") % self.applied[-1].name)
1559 1563 else:
1560 1564 self.ui.write(_("patch queue now empty\n"))
1561 1565 finally:
1562 1566 wlock.release()
1563 1567
1564 1568 def diff(self, repo, pats, opts):
1565 1569 top, patch = self.checktoppatch(repo)
1566 1570 if not top:
1567 1571 self.ui.write(_("no patches applied\n"))
1568 1572 return
1569 1573 qp = self.qparents(repo, top)
1570 1574 if opts.get('reverse'):
1571 1575 node1, node2 = None, qp
1572 1576 else:
1573 1577 node1, node2 = qp, None
1574 1578 diffopts = self.diffopts(opts, patch)
1575 1579 self.printdiff(repo, diffopts, node1, node2, files=pats, opts=opts)
1576 1580
1577 1581 def refresh(self, repo, pats=None, **opts):
1578 1582 if not self.applied:
1579 1583 self.ui.write(_("no patches applied\n"))
1580 1584 return 1
1581 1585 msg = opts.get('msg', '').rstrip()
1582 1586 edit = opts.get('edit')
1583 1587 editform = opts.get('editform', 'mq.qrefresh')
1584 1588 newuser = opts.get('user')
1585 1589 newdate = opts.get('date')
1586 1590 if newdate:
1587 1591 newdate = '%d %d' % util.parsedate(newdate)
1588 1592 wlock = repo.wlock()
1589 1593
1590 1594 try:
1591 1595 self.checktoppatch(repo)
1592 1596 (top, patchfn) = (self.applied[-1].node, self.applied[-1].name)
1593 1597 if repo.changelog.heads(top) != [top]:
1594 1598 raise util.Abort(_("cannot refresh a revision with children"))
1595 1599 if not repo[top].mutable():
1596 1600 raise util.Abort(_("cannot refresh immutable revision"),
1597 1601 hint=_('see "hg help phases" for details'))
1598 1602
1599 1603 cparents = repo.changelog.parents(top)
1600 1604 patchparent = self.qparents(repo, top)
1601 1605
1602 1606 inclsubs = checksubstate(repo, hex(patchparent))
1603 1607 if inclsubs:
1604 1608 substatestate = repo.dirstate['.hgsubstate']
1605 1609
1606 1610 ph = patchheader(self.join(patchfn), self.plainmode)
1607 1611 diffopts = self.diffopts({'git': opts.get('git')}, patchfn)
1608 1612 if newuser:
1609 1613 ph.setuser(newuser)
1610 1614 if newdate:
1611 1615 ph.setdate(newdate)
1612 1616 ph.setparent(hex(patchparent))
1613 1617
1614 1618 # only commit new patch when write is complete
1615 1619 patchf = self.opener(patchfn, 'w', atomictemp=True)
1616 1620
1617 1621 # update the dirstate in place, strip off the qtip commit
1618 1622 # and then commit.
1619 1623 #
1620 1624 # this should really read:
1621 1625 # mm, dd, aa = repo.status(top, patchparent)[:3]
1622 1626 # but we do it backwards to take advantage of manifest/changelog
1623 1627 # caching against the next repo.status call
1624 1628 mm, aa, dd = repo.status(patchparent, top)[:3]
1625 1629 changes = repo.changelog.read(top)
1626 1630 man = repo.manifest.read(changes[0])
1627 1631 aaa = aa[:]
1628 1632 matchfn = scmutil.match(repo[None], pats, opts)
1629 1633 # in short mode, we only diff the files included in the
1630 1634 # patch already plus specified files
1631 1635 if opts.get('short'):
1632 1636 # if amending a patch, we start with existing
1633 1637 # files plus specified files - unfiltered
1634 1638 match = scmutil.matchfiles(repo, mm + aa + dd + matchfn.files())
1635 1639 # filter with include/exclude options
1636 1640 matchfn = scmutil.match(repo[None], opts=opts)
1637 1641 else:
1638 1642 match = scmutil.matchall(repo)
1639 1643 m, a, r, d = repo.status(match=match)[:4]
1640 1644 mm = set(mm)
1641 1645 aa = set(aa)
1642 1646 dd = set(dd)
1643 1647
1644 1648 # we might end up with files that were added between
1645 1649 # qtip and the dirstate parent, but then changed in the
1646 1650 # local dirstate. in this case, we want them to only
1647 1651 # show up in the added section
1648 1652 for x in m:
1649 1653 if x not in aa:
1650 1654 mm.add(x)
1651 1655 # we might end up with files added by the local dirstate that
1652 1656 # were deleted by the patch. In this case, they should only
1653 1657 # show up in the changed section.
1654 1658 for x in a:
1655 1659 if x in dd:
1656 1660 dd.remove(x)
1657 1661 mm.add(x)
1658 1662 else:
1659 1663 aa.add(x)
1660 1664 # make sure any files deleted in the local dirstate
1661 1665 # are not in the add or change column of the patch
1662 1666 forget = []
1663 1667 for x in d + r:
1664 1668 if x in aa:
1665 1669 aa.remove(x)
1666 1670 forget.append(x)
1667 1671 continue
1668 1672 else:
1669 1673 mm.discard(x)
1670 1674 dd.add(x)
1671 1675
1672 1676 m = list(mm)
1673 1677 r = list(dd)
1674 1678 a = list(aa)
1675 1679
1676 1680 # create 'match' that includes the files to be recommitted.
1677 1681 # apply matchfn via repo.status to ensure correct case handling.
1678 1682 cm, ca, cr, cd = repo.status(patchparent, match=matchfn)[:4]
1679 1683 allmatches = set(cm + ca + cr + cd)
1680 1684 refreshchanges = [x.intersection(allmatches) for x in (mm, aa, dd)]
1681 1685
1682 1686 files = set(inclsubs)
1683 1687 for x in refreshchanges:
1684 1688 files.update(x)
1685 1689 match = scmutil.matchfiles(repo, files)
1686 1690
1687 1691 bmlist = repo[top].bookmarks()
1688 1692
1689 1693 dsguard = None
1690 1694 try:
1691 1695 dsguard = cmdutil.dirstateguard(repo, 'mq.refresh')
1692 1696 if diffopts.git or diffopts.upgrade:
1693 1697 copies = {}
1694 1698 for dst in a:
1695 1699 src = repo.dirstate.copied(dst)
1696 1700 # during qfold, the source file for copies may
1697 1701 # be removed. Treat this as a simple add.
1698 1702 if src is not None and src in repo.dirstate:
1699 1703 copies.setdefault(src, []).append(dst)
1700 1704 repo.dirstate.add(dst)
1701 1705 # remember the copies between patchparent and qtip
1702 1706 for dst in aaa:
1703 1707 f = repo.file(dst)
1704 1708 src = f.renamed(man[dst])
1705 1709 if src:
1706 1710 copies.setdefault(src[0], []).extend(
1707 1711 copies.get(dst, []))
1708 1712 if dst in a:
1709 1713 copies[src[0]].append(dst)
1710 1714 # we can't copy a file created by the patch itself
1711 1715 if dst in copies:
1712 1716 del copies[dst]
1713 1717 for src, dsts in copies.iteritems():
1714 1718 for dst in dsts:
1715 1719 repo.dirstate.copy(src, dst)
1716 1720 else:
1717 1721 for dst in a:
1718 1722 repo.dirstate.add(dst)
1719 1723 # Drop useless copy information
1720 1724 for f in list(repo.dirstate.copies()):
1721 1725 repo.dirstate.copy(None, f)
1722 1726 for f in r:
1723 1727 repo.dirstate.remove(f)
1724 1728 # if the patch excludes a modified file, mark that
1725 1729 # file with mtime=0 so status can see it.
1726 1730 mm = []
1727 1731 for i in xrange(len(m) - 1, -1, -1):
1728 1732 if not matchfn(m[i]):
1729 1733 mm.append(m[i])
1730 1734 del m[i]
1731 1735 for f in m:
1732 1736 repo.dirstate.normal(f)
1733 1737 for f in mm:
1734 1738 repo.dirstate.normallookup(f)
1735 1739 for f in forget:
1736 1740 repo.dirstate.drop(f)
1737 1741
1738 1742 user = ph.user or changes[1]
1739 1743
1740 1744 oldphase = repo[top].phase()
1741 1745
1742 1746 # assumes strip can roll itself back if interrupted
1743 1747 repo.setparents(*cparents)
1744 1748 self.applied.pop()
1745 1749 self.applieddirty = True
1746 1750 strip(self.ui, repo, [top], update=False, backup=False)
1747 1751 dsguard.close()
1748 1752 finally:
1749 1753 release(dsguard)
1750 1754
1751 1755 try:
1752 1756 # might be nice to attempt to roll back strip after this
1753 1757
1754 1758 defaultmsg = "[mq]: %s" % patchfn
1755 1759 editor = cmdutil.getcommiteditor(editform=editform)
1756 1760 if edit:
1757 1761 def finishdesc(desc):
1758 1762 if desc.rstrip():
1759 1763 ph.setmessage(desc)
1760 1764 return desc
1761 1765 return defaultmsg
1762 1766 # i18n: this message is shown in editor with "HG: " prefix
1763 1767 extramsg = _('Leave message empty to use default message.')
1764 1768 editor = cmdutil.getcommiteditor(finishdesc=finishdesc,
1765 1769 extramsg=extramsg,
1766 1770 editform=editform)
1767 1771 message = msg or "\n".join(ph.message)
1768 1772 elif not msg:
1769 1773 if not ph.message:
1770 1774 message = defaultmsg
1771 1775 else:
1772 1776 message = "\n".join(ph.message)
1773 1777 else:
1774 1778 message = msg
1775 1779 ph.setmessage(msg)
1776 1780
1777 1781 # Ensure we create a new changeset in the same phase than
1778 1782 # the old one.
1779 1783 n = newcommit(repo, oldphase, message, user, ph.date,
1780 1784 match=match, force=True, editor=editor)
1781 1785 # only write patch after a successful commit
1782 1786 c = [list(x) for x in refreshchanges]
1783 1787 if inclsubs:
1784 1788 self.putsubstate2changes(substatestate, c)
1785 1789 chunks = patchmod.diff(repo, patchparent,
1786 1790 changes=c, opts=diffopts)
1787 1791 comments = str(ph)
1788 1792 if comments:
1789 1793 patchf.write(comments)
1790 1794 for chunk in chunks:
1791 1795 patchf.write(chunk)
1792 1796 patchf.close()
1793 1797
1794 1798 marks = repo._bookmarks
1795 1799 for bm in bmlist:
1796 1800 marks[bm] = n
1797 1801 marks.write()
1798 1802
1799 1803 self.applied.append(statusentry(n, patchfn))
1800 1804 except: # re-raises
1801 1805 ctx = repo[cparents[0]]
1802 1806 repo.dirstate.rebuild(ctx.node(), ctx.manifest())
1803 1807 self.savedirty()
1804 1808 self.ui.warn(_('refresh interrupted while patch was popped! '
1805 1809 '(revert --all, qpush to recover)\n'))
1806 1810 raise
1807 1811 finally:
1808 1812 wlock.release()
1809 1813 self.removeundo(repo)
1810 1814
1811 1815 def init(self, repo, create=False):
1812 1816 if not create and os.path.isdir(self.path):
1813 1817 raise util.Abort(_("patch queue directory already exists"))
1814 1818 try:
1815 1819 os.mkdir(self.path)
1816 1820 except OSError, inst:
1817 1821 if inst.errno != errno.EEXIST or not create:
1818 1822 raise
1819 1823 if create:
1820 1824 return self.qrepo(create=True)
1821 1825
1822 1826 def unapplied(self, repo, patch=None):
1823 1827 if patch and patch not in self.series:
1824 1828 raise util.Abort(_("patch %s is not in series file") % patch)
1825 1829 if not patch:
1826 1830 start = self.seriesend()
1827 1831 else:
1828 1832 start = self.series.index(patch) + 1
1829 1833 unapplied = []
1830 1834 for i in xrange(start, len(self.series)):
1831 1835 pushable, reason = self.pushable(i)
1832 1836 if pushable:
1833 1837 unapplied.append((i, self.series[i]))
1834 1838 self.explainpushable(i)
1835 1839 return unapplied
1836 1840
1837 1841 def qseries(self, repo, missing=None, start=0, length=None, status=None,
1838 1842 summary=False):
1839 1843 def displayname(pfx, patchname, state):
1840 1844 if pfx:
1841 1845 self.ui.write(pfx)
1842 1846 if summary:
1843 1847 ph = patchheader(self.join(patchname), self.plainmode)
1844 1848 if ph.message:
1845 1849 msg = ph.message[0]
1846 1850 else:
1847 1851 msg = ''
1848 1852
1849 1853 if self.ui.formatted():
1850 1854 width = self.ui.termwidth() - len(pfx) - len(patchname) - 2
1851 1855 if width > 0:
1852 1856 msg = util.ellipsis(msg, width)
1853 1857 else:
1854 1858 msg = ''
1855 1859 self.ui.write(patchname, label='qseries.' + state)
1856 1860 self.ui.write(': ')
1857 1861 self.ui.write(msg, label='qseries.message.' + state)
1858 1862 else:
1859 1863 self.ui.write(patchname, label='qseries.' + state)
1860 1864 self.ui.write('\n')
1861 1865
1862 1866 applied = set([p.name for p in self.applied])
1863 1867 if length is None:
1864 1868 length = len(self.series) - start
1865 1869 if not missing:
1866 1870 if self.ui.verbose:
1867 1871 idxwidth = len(str(start + length - 1))
1868 1872 for i in xrange(start, start + length):
1869 1873 patch = self.series[i]
1870 1874 if patch in applied:
1871 1875 char, state = 'A', 'applied'
1872 1876 elif self.pushable(i)[0]:
1873 1877 char, state = 'U', 'unapplied'
1874 1878 else:
1875 1879 char, state = 'G', 'guarded'
1876 1880 pfx = ''
1877 1881 if self.ui.verbose:
1878 1882 pfx = '%*d %s ' % (idxwidth, i, char)
1879 1883 elif status and status != char:
1880 1884 continue
1881 1885 displayname(pfx, patch, state)
1882 1886 else:
1883 1887 msng_list = []
1884 1888 for root, dirs, files in os.walk(self.path):
1885 1889 d = root[len(self.path) + 1:]
1886 1890 for f in files:
1887 1891 fl = os.path.join(d, f)
1888 1892 if (fl not in self.series and
1889 1893 fl not in (self.statuspath, self.seriespath,
1890 1894 self.guardspath)
1891 1895 and not fl.startswith('.')):
1892 1896 msng_list.append(fl)
1893 1897 for x in sorted(msng_list):
1894 1898 pfx = self.ui.verbose and ('D ') or ''
1895 1899 displayname(pfx, x, 'missing')
1896 1900
1897 1901 def issaveline(self, l):
1898 1902 if l.name == '.hg.patches.save.line':
1899 1903 return True
1900 1904
1901 1905 def qrepo(self, create=False):
1902 1906 ui = self.baseui.copy()
1903 1907 if create or os.path.isdir(self.join(".hg")):
1904 1908 return hg.repository(ui, path=self.path, create=create)
1905 1909
1906 1910 def restore(self, repo, rev, delete=None, qupdate=None):
1907 1911 desc = repo[rev].description().strip()
1908 1912 lines = desc.splitlines()
1909 1913 i = 0
1910 1914 datastart = None
1911 1915 series = []
1912 1916 applied = []
1913 1917 qpp = None
1914 1918 for i, line in enumerate(lines):
1915 1919 if line == 'Patch Data:':
1916 1920 datastart = i + 1
1917 1921 elif line.startswith('Dirstate:'):
1918 1922 l = line.rstrip()
1919 1923 l = l[10:].split(' ')
1920 1924 qpp = [bin(x) for x in l]
1921 1925 elif datastart is not None:
1922 1926 l = line.rstrip()
1923 1927 n, name = l.split(':', 1)
1924 1928 if n:
1925 1929 applied.append(statusentry(bin(n), name))
1926 1930 else:
1927 1931 series.append(l)
1928 1932 if datastart is None:
1929 1933 self.ui.warn(_("no saved patch data found\n"))
1930 1934 return 1
1931 1935 self.ui.warn(_("restoring status: %s\n") % lines[0])
1932 1936 self.fullseries = series
1933 1937 self.applied = applied
1934 1938 self.parseseries()
1935 1939 self.seriesdirty = True
1936 1940 self.applieddirty = True
1937 1941 heads = repo.changelog.heads()
1938 1942 if delete:
1939 1943 if rev not in heads:
1940 1944 self.ui.warn(_("save entry has children, leaving it alone\n"))
1941 1945 else:
1942 1946 self.ui.warn(_("removing save entry %s\n") % short(rev))
1943 1947 pp = repo.dirstate.parents()
1944 1948 if rev in pp:
1945 1949 update = True
1946 1950 else:
1947 1951 update = False
1948 1952 strip(self.ui, repo, [rev], update=update, backup=False)
1949 1953 if qpp:
1950 1954 self.ui.warn(_("saved queue repository parents: %s %s\n") %
1951 1955 (short(qpp[0]), short(qpp[1])))
1952 1956 if qupdate:
1953 1957 self.ui.status(_("updating queue directory\n"))
1954 1958 r = self.qrepo()
1955 1959 if not r:
1956 1960 self.ui.warn(_("unable to load queue repository\n"))
1957 1961 return 1
1958 1962 hg.clean(r, qpp[0])
1959 1963
1960 1964 def save(self, repo, msg=None):
1961 1965 if not self.applied:
1962 1966 self.ui.warn(_("save: no patches applied, exiting\n"))
1963 1967 return 1
1964 1968 if self.issaveline(self.applied[-1]):
1965 1969 self.ui.warn(_("status is already saved\n"))
1966 1970 return 1
1967 1971
1968 1972 if not msg:
1969 1973 msg = _("hg patches saved state")
1970 1974 else:
1971 1975 msg = "hg patches: " + msg.rstrip('\r\n')
1972 1976 r = self.qrepo()
1973 1977 if r:
1974 1978 pp = r.dirstate.parents()
1975 1979 msg += "\nDirstate: %s %s" % (hex(pp[0]), hex(pp[1]))
1976 1980 msg += "\n\nPatch Data:\n"
1977 1981 msg += ''.join('%s\n' % x for x in self.applied)
1978 1982 msg += ''.join(':%s\n' % x for x in self.fullseries)
1979 1983 n = repo.commit(msg, force=True)
1980 1984 if not n:
1981 1985 self.ui.warn(_("repo commit failed\n"))
1982 1986 return 1
1983 1987 self.applied.append(statusentry(n, '.hg.patches.save.line'))
1984 1988 self.applieddirty = True
1985 1989 self.removeundo(repo)
1986 1990
1987 1991 def fullseriesend(self):
1988 1992 if self.applied:
1989 1993 p = self.applied[-1].name
1990 1994 end = self.findseries(p)
1991 1995 if end is None:
1992 1996 return len(self.fullseries)
1993 1997 return end + 1
1994 1998 return 0
1995 1999
1996 2000 def seriesend(self, all_patches=False):
1997 2001 """If all_patches is False, return the index of the next pushable patch
1998 2002 in the series, or the series length. If all_patches is True, return the
1999 2003 index of the first patch past the last applied one.
2000 2004 """
2001 2005 end = 0
2002 2006 def nextpatch(start):
2003 2007 if all_patches or start >= len(self.series):
2004 2008 return start
2005 2009 for i in xrange(start, len(self.series)):
2006 2010 p, reason = self.pushable(i)
2007 2011 if p:
2008 2012 return i
2009 2013 self.explainpushable(i)
2010 2014 return len(self.series)
2011 2015 if self.applied:
2012 2016 p = self.applied[-1].name
2013 2017 try:
2014 2018 end = self.series.index(p)
2015 2019 except ValueError:
2016 2020 return 0
2017 2021 return nextpatch(end + 1)
2018 2022 return nextpatch(end)
2019 2023
2020 2024 def appliedname(self, index):
2021 2025 pname = self.applied[index].name
2022 2026 if not self.ui.verbose:
2023 2027 p = pname
2024 2028 else:
2025 2029 p = str(self.series.index(pname)) + " " + pname
2026 2030 return p
2027 2031
2028 2032 def qimport(self, repo, files, patchname=None, rev=None, existing=None,
2029 2033 force=None, git=False):
2030 2034 def checkseries(patchname):
2031 2035 if patchname in self.series:
2032 2036 raise util.Abort(_('patch %s is already in the series file')
2033 2037 % patchname)
2034 2038
2035 2039 if rev:
2036 2040 if files:
2037 2041 raise util.Abort(_('option "-r" not valid when importing '
2038 2042 'files'))
2039 2043 rev = scmutil.revrange(repo, rev)
2040 2044 rev.sort(reverse=True)
2041 2045 elif not files:
2042 2046 raise util.Abort(_('no files or revisions specified'))
2043 2047 if (len(files) > 1 or len(rev) > 1) and patchname:
2044 2048 raise util.Abort(_('option "-n" not valid when importing multiple '
2045 2049 'patches'))
2046 2050 imported = []
2047 2051 if rev:
2048 2052 # If mq patches are applied, we can only import revisions
2049 2053 # that form a linear path to qbase.
2050 2054 # Otherwise, they should form a linear path to a head.
2051 2055 heads = repo.changelog.heads(repo.changelog.node(rev.first()))
2052 2056 if len(heads) > 1:
2053 2057 raise util.Abort(_('revision %d is the root of more than one '
2054 2058 'branch') % rev.last())
2055 2059 if self.applied:
2056 2060 base = repo.changelog.node(rev.first())
2057 2061 if base in [n.node for n in self.applied]:
2058 2062 raise util.Abort(_('revision %d is already managed')
2059 2063 % rev.first())
2060 2064 if heads != [self.applied[-1].node]:
2061 2065 raise util.Abort(_('revision %d is not the parent of '
2062 2066 'the queue') % rev.first())
2063 2067 base = repo.changelog.rev(self.applied[0].node)
2064 2068 lastparent = repo.changelog.parentrevs(base)[0]
2065 2069 else:
2066 2070 if heads != [repo.changelog.node(rev.first())]:
2067 2071 raise util.Abort(_('revision %d has unmanaged children')
2068 2072 % rev.first())
2069 2073 lastparent = None
2070 2074
2071 2075 diffopts = self.diffopts({'git': git})
2072 2076 tr = repo.transaction('qimport')
2073 2077 try:
2074 2078 for r in rev:
2075 2079 if not repo[r].mutable():
2076 2080 raise util.Abort(_('revision %d is not mutable') % r,
2077 2081 hint=_('see "hg help phases" '
2078 2082 'for details'))
2079 2083 p1, p2 = repo.changelog.parentrevs(r)
2080 2084 n = repo.changelog.node(r)
2081 2085 if p2 != nullrev:
2082 2086 raise util.Abort(_('cannot import merge revision %d')
2083 2087 % r)
2084 2088 if lastparent and lastparent != r:
2085 2089 raise util.Abort(_('revision %d is not the parent of '
2086 2090 '%d')
2087 2091 % (r, lastparent))
2088 2092 lastparent = p1
2089 2093
2090 2094 if not patchname:
2091 2095 patchname = normname('%d.diff' % r)
2092 2096 checkseries(patchname)
2093 2097 self.checkpatchname(patchname, force)
2094 2098 self.fullseries.insert(0, patchname)
2095 2099
2096 2100 patchf = self.opener(patchname, "w")
2097 2101 cmdutil.export(repo, [n], fp=patchf, opts=diffopts)
2098 2102 patchf.close()
2099 2103
2100 2104 se = statusentry(n, patchname)
2101 2105 self.applied.insert(0, se)
2102 2106
2103 2107 self.added.append(patchname)
2104 2108 imported.append(patchname)
2105 2109 patchname = None
2106 2110 if rev and repo.ui.configbool('mq', 'secret', False):
2107 2111 # if we added anything with --rev, move the secret root
2108 2112 phases.retractboundary(repo, tr, phases.secret, [n])
2109 2113 self.parseseries()
2110 2114 self.applieddirty = True
2111 2115 self.seriesdirty = True
2112 2116 tr.close()
2113 2117 finally:
2114 2118 tr.release()
2115 2119
2116 2120 for i, filename in enumerate(files):
2117 2121 if existing:
2118 2122 if filename == '-':
2119 2123 raise util.Abort(_('-e is incompatible with import from -'))
2120 2124 filename = normname(filename)
2121 2125 self.checkreservedname(filename)
2122 2126 if util.url(filename).islocal():
2123 2127 originpath = self.join(filename)
2124 2128 if not os.path.isfile(originpath):
2125 2129 raise util.Abort(
2126 2130 _("patch %s does not exist") % filename)
2127 2131
2128 2132 if patchname:
2129 2133 self.checkpatchname(patchname, force)
2130 2134
2131 2135 self.ui.write(_('renaming %s to %s\n')
2132 2136 % (filename, patchname))
2133 2137 util.rename(originpath, self.join(patchname))
2134 2138 else:
2135 2139 patchname = filename
2136 2140
2137 2141 else:
2138 2142 if filename == '-' and not patchname:
2139 2143 raise util.Abort(_('need --name to import a patch from -'))
2140 2144 elif not patchname:
2141 2145 patchname = normname(os.path.basename(filename.rstrip('/')))
2142 2146 self.checkpatchname(patchname, force)
2143 2147 try:
2144 2148 if filename == '-':
2145 2149 text = self.ui.fin.read()
2146 2150 else:
2147 2151 fp = hg.openpath(self.ui, filename)
2148 2152 text = fp.read()
2149 2153 fp.close()
2150 2154 except (OSError, IOError):
2151 2155 raise util.Abort(_("unable to read file %s") % filename)
2152 2156 patchf = self.opener(patchname, "w")
2153 2157 patchf.write(text)
2154 2158 patchf.close()
2155 2159 if not force:
2156 2160 checkseries(patchname)
2157 2161 if patchname not in self.series:
2158 2162 index = self.fullseriesend() + i
2159 2163 self.fullseries[index:index] = [patchname]
2160 2164 self.parseseries()
2161 2165 self.seriesdirty = True
2162 2166 self.ui.warn(_("adding %s to series file\n") % patchname)
2163 2167 self.added.append(patchname)
2164 2168 imported.append(patchname)
2165 2169 patchname = None
2166 2170
2167 2171 self.removeundo(repo)
2168 2172 return imported
2169 2173
2170 2174 def fixkeepchangesopts(ui, opts):
2171 2175 if (not ui.configbool('mq', 'keepchanges') or opts.get('force')
2172 2176 or opts.get('exact')):
2173 2177 return opts
2174 2178 opts = dict(opts)
2175 2179 opts['keep_changes'] = True
2176 2180 return opts
2177 2181
2178 2182 @command("qdelete|qremove|qrm",
2179 2183 [('k', 'keep', None, _('keep patch file')),
2180 2184 ('r', 'rev', [],
2181 2185 _('stop managing a revision (DEPRECATED)'), _('REV'))],
2182 2186 _('hg qdelete [-k] [PATCH]...'))
2183 2187 def delete(ui, repo, *patches, **opts):
2184 2188 """remove patches from queue
2185 2189
2186 2190 The patches must not be applied, and at least one patch is required. Exact
2187 2191 patch identifiers must be given. With -k/--keep, the patch files are
2188 2192 preserved in the patch directory.
2189 2193
2190 2194 To stop managing a patch and move it into permanent history,
2191 2195 use the :hg:`qfinish` command."""
2192 2196 q = repo.mq
2193 2197 q.delete(repo, patches, opts)
2194 2198 q.savedirty()
2195 2199 return 0
2196 2200
2197 2201 @command("qapplied",
2198 2202 [('1', 'last', None, _('show only the preceding applied patch'))
2199 2203 ] + seriesopts,
2200 2204 _('hg qapplied [-1] [-s] [PATCH]'))
2201 2205 def applied(ui, repo, patch=None, **opts):
2202 2206 """print the patches already applied
2203 2207
2204 2208 Returns 0 on success."""
2205 2209
2206 2210 q = repo.mq
2207 2211
2208 2212 if patch:
2209 2213 if patch not in q.series:
2210 2214 raise util.Abort(_("patch %s is not in series file") % patch)
2211 2215 end = q.series.index(patch) + 1
2212 2216 else:
2213 2217 end = q.seriesend(True)
2214 2218
2215 2219 if opts.get('last') and not end:
2216 2220 ui.write(_("no patches applied\n"))
2217 2221 return 1
2218 2222 elif opts.get('last') and end == 1:
2219 2223 ui.write(_("only one patch applied\n"))
2220 2224 return 1
2221 2225 elif opts.get('last'):
2222 2226 start = end - 2
2223 2227 end = 1
2224 2228 else:
2225 2229 start = 0
2226 2230
2227 2231 q.qseries(repo, length=end, start=start, status='A',
2228 2232 summary=opts.get('summary'))
2229 2233
2230 2234
2231 2235 @command("qunapplied",
2232 2236 [('1', 'first', None, _('show only the first patch'))] + seriesopts,
2233 2237 _('hg qunapplied [-1] [-s] [PATCH]'))
2234 2238 def unapplied(ui, repo, patch=None, **opts):
2235 2239 """print the patches not yet applied
2236 2240
2237 2241 Returns 0 on success."""
2238 2242
2239 2243 q = repo.mq
2240 2244 if patch:
2241 2245 if patch not in q.series:
2242 2246 raise util.Abort(_("patch %s is not in series file") % patch)
2243 2247 start = q.series.index(patch) + 1
2244 2248 else:
2245 2249 start = q.seriesend(True)
2246 2250
2247 2251 if start == len(q.series) and opts.get('first'):
2248 2252 ui.write(_("all patches applied\n"))
2249 2253 return 1
2250 2254
2251 2255 if opts.get('first'):
2252 2256 length = 1
2253 2257 else:
2254 2258 length = None
2255 2259 q.qseries(repo, start=start, length=length, status='U',
2256 2260 summary=opts.get('summary'))
2257 2261
2258 2262 @command("qimport",
2259 2263 [('e', 'existing', None, _('import file in patch directory')),
2260 2264 ('n', 'name', '',
2261 2265 _('name of patch file'), _('NAME')),
2262 2266 ('f', 'force', None, _('overwrite existing files')),
2263 2267 ('r', 'rev', [],
2264 2268 _('place existing revisions under mq control'), _('REV')),
2265 2269 ('g', 'git', None, _('use git extended diff format')),
2266 2270 ('P', 'push', None, _('qpush after importing'))],
2267 2271 _('hg qimport [-e] [-n NAME] [-f] [-g] [-P] [-r REV]... [FILE]...'))
2268 2272 def qimport(ui, repo, *filename, **opts):
2269 2273 """import a patch or existing changeset
2270 2274
2271 2275 The patch is inserted into the series after the last applied
2272 2276 patch. If no patches have been applied, qimport prepends the patch
2273 2277 to the series.
2274 2278
2275 2279 The patch will have the same name as its source file unless you
2276 2280 give it a new one with -n/--name.
2277 2281
2278 2282 You can register an existing patch inside the patch directory with
2279 2283 the -e/--existing flag.
2280 2284
2281 2285 With -f/--force, an existing patch of the same name will be
2282 2286 overwritten.
2283 2287
2284 2288 An existing changeset may be placed under mq control with -r/--rev
2285 2289 (e.g. qimport --rev . -n patch will place the current revision
2286 2290 under mq control). With -g/--git, patches imported with --rev will
2287 2291 use the git diff format. See the diffs help topic for information
2288 2292 on why this is important for preserving rename/copy information
2289 2293 and permission changes. Use :hg:`qfinish` to remove changesets
2290 2294 from mq control.
2291 2295
2292 2296 To import a patch from standard input, pass - as the patch file.
2293 2297 When importing from standard input, a patch name must be specified
2294 2298 using the --name flag.
2295 2299
2296 2300 To import an existing patch while renaming it::
2297 2301
2298 2302 hg qimport -e existing-patch -n new-name
2299 2303
2300 2304 Returns 0 if import succeeded.
2301 2305 """
2302 2306 lock = repo.lock() # cause this may move phase
2303 2307 try:
2304 2308 q = repo.mq
2305 2309 try:
2306 2310 imported = q.qimport(
2307 2311 repo, filename, patchname=opts.get('name'),
2308 2312 existing=opts.get('existing'), force=opts.get('force'),
2309 2313 rev=opts.get('rev'), git=opts.get('git'))
2310 2314 finally:
2311 2315 q.savedirty()
2312 2316 finally:
2313 2317 lock.release()
2314 2318
2315 2319 if imported and opts.get('push') and not opts.get('rev'):
2316 2320 return q.push(repo, imported[-1])
2317 2321 return 0
2318 2322
2319 2323 def qinit(ui, repo, create):
2320 2324 """initialize a new queue repository
2321 2325
2322 2326 This command also creates a series file for ordering patches, and
2323 2327 an mq-specific .hgignore file in the queue repository, to exclude
2324 2328 the status and guards files (these contain mostly transient state).
2325 2329
2326 2330 Returns 0 if initialization succeeded."""
2327 2331 q = repo.mq
2328 2332 r = q.init(repo, create)
2329 2333 q.savedirty()
2330 2334 if r:
2331 2335 if not os.path.exists(r.wjoin('.hgignore')):
2332 2336 fp = r.wvfs('.hgignore', 'w')
2333 2337 fp.write('^\\.hg\n')
2334 2338 fp.write('^\\.mq\n')
2335 2339 fp.write('syntax: glob\n')
2336 2340 fp.write('status\n')
2337 2341 fp.write('guards\n')
2338 2342 fp.close()
2339 2343 if not os.path.exists(r.wjoin('series')):
2340 2344 r.wvfs('series', 'w').close()
2341 2345 r[None].add(['.hgignore', 'series'])
2342 2346 commands.add(ui, r)
2343 2347 return 0
2344 2348
2345 2349 @command("^qinit",
2346 2350 [('c', 'create-repo', None, _('create queue repository'))],
2347 2351 _('hg qinit [-c]'))
2348 2352 def init(ui, repo, **opts):
2349 2353 """init a new queue repository (DEPRECATED)
2350 2354
2351 2355 The queue repository is unversioned by default. If
2352 2356 -c/--create-repo is specified, qinit will create a separate nested
2353 2357 repository for patches (qinit -c may also be run later to convert
2354 2358 an unversioned patch repository into a versioned one). You can use
2355 2359 qcommit to commit changes to this queue repository.
2356 2360
2357 2361 This command is deprecated. Without -c, it's implied by other relevant
2358 2362 commands. With -c, use :hg:`init --mq` instead."""
2359 2363 return qinit(ui, repo, create=opts.get('create_repo'))
2360 2364
2361 2365 @command("qclone",
2362 2366 [('', 'pull', None, _('use pull protocol to copy metadata')),
2363 2367 ('U', 'noupdate', None,
2364 2368 _('do not update the new working directories')),
2365 2369 ('', 'uncompressed', None,
2366 2370 _('use uncompressed transfer (fast over LAN)')),
2367 2371 ('p', 'patches', '',
2368 2372 _('location of source patch repository'), _('REPO')),
2369 2373 ] + commands.remoteopts,
2370 2374 _('hg qclone [OPTION]... SOURCE [DEST]'),
2371 2375 norepo=True)
2372 2376 def clone(ui, source, dest=None, **opts):
2373 2377 '''clone main and patch repository at same time
2374 2378
2375 2379 If source is local, destination will have no patches applied. If
2376 2380 source is remote, this command can not check if patches are
2377 2381 applied in source, so cannot guarantee that patches are not
2378 2382 applied in destination. If you clone remote repository, be sure
2379 2383 before that it has no patches applied.
2380 2384
2381 2385 Source patch repository is looked for in <src>/.hg/patches by
2382 2386 default. Use -p <url> to change.
2383 2387
2384 2388 The patch directory must be a nested Mercurial repository, as
2385 2389 would be created by :hg:`init --mq`.
2386 2390
2387 2391 Return 0 on success.
2388 2392 '''
2389 2393 def patchdir(repo):
2390 2394 """compute a patch repo url from a repo object"""
2391 2395 url = repo.url()
2392 2396 if url.endswith('/'):
2393 2397 url = url[:-1]
2394 2398 return url + '/.hg/patches'
2395 2399
2396 2400 # main repo (destination and sources)
2397 2401 if dest is None:
2398 2402 dest = hg.defaultdest(source)
2399 2403 sr = hg.peer(ui, opts, ui.expandpath(source))
2400 2404
2401 2405 # patches repo (source only)
2402 2406 if opts.get('patches'):
2403 2407 patchespath = ui.expandpath(opts.get('patches'))
2404 2408 else:
2405 2409 patchespath = patchdir(sr)
2406 2410 try:
2407 2411 hg.peer(ui, opts, patchespath)
2408 2412 except error.RepoError:
2409 2413 raise util.Abort(_('versioned patch repository not found'
2410 2414 ' (see init --mq)'))
2411 2415 qbase, destrev = None, None
2412 2416 if sr.local():
2413 2417 repo = sr.local()
2414 2418 if repo.mq.applied and repo[qbase].phase() != phases.secret:
2415 2419 qbase = repo.mq.applied[0].node
2416 2420 if not hg.islocal(dest):
2417 2421 heads = set(repo.heads())
2418 2422 destrev = list(heads.difference(repo.heads(qbase)))
2419 2423 destrev.append(repo.changelog.parents(qbase)[0])
2420 2424 elif sr.capable('lookup'):
2421 2425 try:
2422 2426 qbase = sr.lookup('qbase')
2423 2427 except error.RepoError:
2424 2428 pass
2425 2429
2426 2430 ui.note(_('cloning main repository\n'))
2427 2431 sr, dr = hg.clone(ui, opts, sr.url(), dest,
2428 2432 pull=opts.get('pull'),
2429 2433 rev=destrev,
2430 2434 update=False,
2431 2435 stream=opts.get('uncompressed'))
2432 2436
2433 2437 ui.note(_('cloning patch repository\n'))
2434 2438 hg.clone(ui, opts, opts.get('patches') or patchdir(sr), patchdir(dr),
2435 2439 pull=opts.get('pull'), update=not opts.get('noupdate'),
2436 2440 stream=opts.get('uncompressed'))
2437 2441
2438 2442 if dr.local():
2439 2443 repo = dr.local()
2440 2444 if qbase:
2441 2445 ui.note(_('stripping applied patches from destination '
2442 2446 'repository\n'))
2443 2447 strip(ui, repo, [qbase], update=False, backup=None)
2444 2448 if not opts.get('noupdate'):
2445 2449 ui.note(_('updating destination repository\n'))
2446 2450 hg.update(repo, repo.changelog.tip())
2447 2451
2448 2452 @command("qcommit|qci",
2449 2453 commands.table["^commit|ci"][1],
2450 2454 _('hg qcommit [OPTION]... [FILE]...'),
2451 2455 inferrepo=True)
2452 2456 def commit(ui, repo, *pats, **opts):
2453 2457 """commit changes in the queue repository (DEPRECATED)
2454 2458
2455 2459 This command is deprecated; use :hg:`commit --mq` instead."""
2456 2460 q = repo.mq
2457 2461 r = q.qrepo()
2458 2462 if not r:
2459 2463 raise util.Abort('no queue repository')
2460 2464 commands.commit(r.ui, r, *pats, **opts)
2461 2465
2462 2466 @command("qseries",
2463 2467 [('m', 'missing', None, _('print patches not in series')),
2464 2468 ] + seriesopts,
2465 2469 _('hg qseries [-ms]'))
2466 2470 def series(ui, repo, **opts):
2467 2471 """print the entire series file
2468 2472
2469 2473 Returns 0 on success."""
2470 2474 repo.mq.qseries(repo, missing=opts.get('missing'),
2471 2475 summary=opts.get('summary'))
2472 2476 return 0
2473 2477
2474 2478 @command("qtop", seriesopts, _('hg qtop [-s]'))
2475 2479 def top(ui, repo, **opts):
2476 2480 """print the name of the current patch
2477 2481
2478 2482 Returns 0 on success."""
2479 2483 q = repo.mq
2480 2484 if q.applied:
2481 2485 t = q.seriesend(True)
2482 2486 else:
2483 2487 t = 0
2484 2488
2485 2489 if t:
2486 2490 q.qseries(repo, start=t - 1, length=1, status='A',
2487 2491 summary=opts.get('summary'))
2488 2492 else:
2489 2493 ui.write(_("no patches applied\n"))
2490 2494 return 1
2491 2495
2492 2496 @command("qnext", seriesopts, _('hg qnext [-s]'))
2493 2497 def next(ui, repo, **opts):
2494 2498 """print the name of the next pushable patch
2495 2499
2496 2500 Returns 0 on success."""
2497 2501 q = repo.mq
2498 2502 end = q.seriesend()
2499 2503 if end == len(q.series):
2500 2504 ui.write(_("all patches applied\n"))
2501 2505 return 1
2502 2506 q.qseries(repo, start=end, length=1, summary=opts.get('summary'))
2503 2507
2504 2508 @command("qprev", seriesopts, _('hg qprev [-s]'))
2505 2509 def prev(ui, repo, **opts):
2506 2510 """print the name of the preceding applied patch
2507 2511
2508 2512 Returns 0 on success."""
2509 2513 q = repo.mq
2510 2514 l = len(q.applied)
2511 2515 if l == 1:
2512 2516 ui.write(_("only one patch applied\n"))
2513 2517 return 1
2514 2518 if not l:
2515 2519 ui.write(_("no patches applied\n"))
2516 2520 return 1
2517 2521 idx = q.series.index(q.applied[-2].name)
2518 2522 q.qseries(repo, start=idx, length=1, status='A',
2519 2523 summary=opts.get('summary'))
2520 2524
2521 2525 def setupheaderopts(ui, opts):
2522 2526 if not opts.get('user') and opts.get('currentuser'):
2523 2527 opts['user'] = ui.username()
2524 2528 if not opts.get('date') and opts.get('currentdate'):
2525 2529 opts['date'] = "%d %d" % util.makedate()
2526 2530
2527 2531 @command("^qnew",
2528 2532 [('e', 'edit', None, _('invoke editor on commit messages')),
2529 2533 ('f', 'force', None, _('import uncommitted changes (DEPRECATED)')),
2530 2534 ('g', 'git', None, _('use git extended diff format')),
2531 2535 ('U', 'currentuser', None, _('add "From: <current user>" to patch')),
2532 2536 ('u', 'user', '',
2533 2537 _('add "From: <USER>" to patch'), _('USER')),
2534 2538 ('D', 'currentdate', None, _('add "Date: <current date>" to patch')),
2535 2539 ('d', 'date', '',
2536 2540 _('add "Date: <DATE>" to patch'), _('DATE'))
2537 2541 ] + commands.walkopts + commands.commitopts,
2538 2542 _('hg qnew [-e] [-m TEXT] [-l FILE] PATCH [FILE]...'),
2539 2543 inferrepo=True)
2540 2544 def new(ui, repo, patch, *args, **opts):
2541 2545 """create a new patch
2542 2546
2543 2547 qnew creates a new patch on top of the currently-applied patch (if
2544 2548 any). The patch will be initialized with any outstanding changes
2545 2549 in the working directory. You may also use -I/--include,
2546 2550 -X/--exclude, and/or a list of files after the patch name to add
2547 2551 only changes to matching files to the new patch, leaving the rest
2548 2552 as uncommitted modifications.
2549 2553
2550 2554 -u/--user and -d/--date can be used to set the (given) user and
2551 2555 date, respectively. -U/--currentuser and -D/--currentdate set user
2552 2556 to current user and date to current date.
2553 2557
2554 2558 -e/--edit, -m/--message or -l/--logfile set the patch header as
2555 2559 well as the commit message. If none is specified, the header is
2556 2560 empty and the commit message is '[mq]: PATCH'.
2557 2561
2558 2562 Use the -g/--git option to keep the patch in the git extended diff
2559 2563 format. Read the diffs help topic for more information on why this
2560 2564 is important for preserving permission changes and copy/rename
2561 2565 information.
2562 2566
2563 2567 Returns 0 on successful creation of a new patch.
2564 2568 """
2565 2569 msg = cmdutil.logmessage(ui, opts)
2566 2570 q = repo.mq
2567 2571 opts['msg'] = msg
2568 2572 setupheaderopts(ui, opts)
2569 2573 q.new(repo, patch, *args, **opts)
2570 2574 q.savedirty()
2571 2575 return 0
2572 2576
2573 2577 @command("^qrefresh",
2574 2578 [('e', 'edit', None, _('invoke editor on commit messages')),
2575 2579 ('g', 'git', None, _('use git extended diff format')),
2576 2580 ('s', 'short', None,
2577 2581 _('refresh only files already in the patch and specified files')),
2578 2582 ('U', 'currentuser', None,
2579 2583 _('add/update author field in patch with current user')),
2580 2584 ('u', 'user', '',
2581 2585 _('add/update author field in patch with given user'), _('USER')),
2582 2586 ('D', 'currentdate', None,
2583 2587 _('add/update date field in patch with current date')),
2584 2588 ('d', 'date', '',
2585 2589 _('add/update date field in patch with given date'), _('DATE'))
2586 2590 ] + commands.walkopts + commands.commitopts,
2587 2591 _('hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] [FILE]...'),
2588 2592 inferrepo=True)
2589 2593 def refresh(ui, repo, *pats, **opts):
2590 2594 """update the current patch
2591 2595
2592 2596 If any file patterns are provided, the refreshed patch will
2593 2597 contain only the modifications that match those patterns; the
2594 2598 remaining modifications will remain in the working directory.
2595 2599
2596 2600 If -s/--short is specified, files currently included in the patch
2597 2601 will be refreshed just like matched files and remain in the patch.
2598 2602
2599 2603 If -e/--edit is specified, Mercurial will start your configured editor for
2600 2604 you to enter a message. In case qrefresh fails, you will find a backup of
2601 2605 your message in ``.hg/last-message.txt``.
2602 2606
2603 2607 hg add/remove/copy/rename work as usual, though you might want to
2604 2608 use git-style patches (-g/--git or [diff] git=1) to track copies
2605 2609 and renames. See the diffs help topic for more information on the
2606 2610 git diff format.
2607 2611
2608 2612 Returns 0 on success.
2609 2613 """
2610 2614 q = repo.mq
2611 2615 message = cmdutil.logmessage(ui, opts)
2612 2616 setupheaderopts(ui, opts)
2613 2617 wlock = repo.wlock()
2614 2618 try:
2615 2619 ret = q.refresh(repo, pats, msg=message, **opts)
2616 2620 q.savedirty()
2617 2621 return ret
2618 2622 finally:
2619 2623 wlock.release()
2620 2624
2621 2625 @command("^qdiff",
2622 2626 commands.diffopts + commands.diffopts2 + commands.walkopts,
2623 2627 _('hg qdiff [OPTION]... [FILE]...'),
2624 2628 inferrepo=True)
2625 2629 def diff(ui, repo, *pats, **opts):
2626 2630 """diff of the current patch and subsequent modifications
2627 2631
2628 2632 Shows a diff which includes the current patch as well as any
2629 2633 changes which have been made in the working directory since the
2630 2634 last refresh (thus showing what the current patch would become
2631 2635 after a qrefresh).
2632 2636
2633 2637 Use :hg:`diff` if you only want to see the changes made since the
2634 2638 last qrefresh, or :hg:`export qtip` if you want to see changes
2635 2639 made by the current patch without including changes made since the
2636 2640 qrefresh.
2637 2641
2638 2642 Returns 0 on success.
2639 2643 """
2640 2644 repo.mq.diff(repo, pats, opts)
2641 2645 return 0
2642 2646
2643 2647 @command('qfold',
2644 2648 [('e', 'edit', None, _('invoke editor on commit messages')),
2645 2649 ('k', 'keep', None, _('keep folded patch files')),
2646 2650 ] + commands.commitopts,
2647 2651 _('hg qfold [-e] [-k] [-m TEXT] [-l FILE] PATCH...'))
2648 2652 def fold(ui, repo, *files, **opts):
2649 2653 """fold the named patches into the current patch
2650 2654
2651 2655 Patches must not yet be applied. Each patch will be successively
2652 2656 applied to the current patch in the order given. If all the
2653 2657 patches apply successfully, the current patch will be refreshed
2654 2658 with the new cumulative patch, and the folded patches will be
2655 2659 deleted. With -k/--keep, the folded patch files will not be
2656 2660 removed afterwards.
2657 2661
2658 2662 The header for each folded patch will be concatenated with the
2659 2663 current patch header, separated by a line of ``* * *``.
2660 2664
2661 2665 Returns 0 on success."""
2662 2666 q = repo.mq
2663 2667 if not files:
2664 2668 raise util.Abort(_('qfold requires at least one patch name'))
2665 2669 if not q.checktoppatch(repo)[0]:
2666 2670 raise util.Abort(_('no patches applied'))
2667 2671 q.checklocalchanges(repo)
2668 2672
2669 2673 message = cmdutil.logmessage(ui, opts)
2670 2674
2671 2675 parent = q.lookup('qtip')
2672 2676 patches = []
2673 2677 messages = []
2674 2678 for f in files:
2675 2679 p = q.lookup(f)
2676 2680 if p in patches or p == parent:
2677 2681 ui.warn(_('skipping already folded patch %s\n') % p)
2678 2682 if q.isapplied(p):
2679 2683 raise util.Abort(_('qfold cannot fold already applied patch %s')
2680 2684 % p)
2681 2685 patches.append(p)
2682 2686
2683 2687 for p in patches:
2684 2688 if not message:
2685 2689 ph = patchheader(q.join(p), q.plainmode)
2686 2690 if ph.message:
2687 2691 messages.append(ph.message)
2688 2692 pf = q.join(p)
2689 2693 (patchsuccess, files, fuzz) = q.patch(repo, pf)
2690 2694 if not patchsuccess:
2691 2695 raise util.Abort(_('error folding patch %s') % p)
2692 2696
2693 2697 if not message:
2694 2698 ph = patchheader(q.join(parent), q.plainmode)
2695 2699 message = ph.message
2696 2700 for msg in messages:
2697 2701 if msg:
2698 2702 if message:
2699 2703 message.append('* * *')
2700 2704 message.extend(msg)
2701 2705 message = '\n'.join(message)
2702 2706
2703 2707 diffopts = q.patchopts(q.diffopts(), *patches)
2704 2708 wlock = repo.wlock()
2705 2709 try:
2706 2710 q.refresh(repo, msg=message, git=diffopts.git, edit=opts.get('edit'),
2707 2711 editform='mq.qfold')
2708 2712 q.delete(repo, patches, opts)
2709 2713 q.savedirty()
2710 2714 finally:
2711 2715 wlock.release()
2712 2716
2713 2717 @command("qgoto",
2714 2718 [('', 'keep-changes', None,
2715 2719 _('tolerate non-conflicting local changes')),
2716 2720 ('f', 'force', None, _('overwrite any local changes')),
2717 2721 ('', 'no-backup', None, _('do not save backup copies of files'))],
2718 2722 _('hg qgoto [OPTION]... PATCH'))
2719 2723 def goto(ui, repo, patch, **opts):
2720 2724 '''push or pop patches until named patch is at top of stack
2721 2725
2722 2726 Returns 0 on success.'''
2723 2727 opts = fixkeepchangesopts(ui, opts)
2724 2728 q = repo.mq
2725 2729 patch = q.lookup(patch)
2726 2730 nobackup = opts.get('no_backup')
2727 2731 keepchanges = opts.get('keep_changes')
2728 2732 if q.isapplied(patch):
2729 2733 ret = q.pop(repo, patch, force=opts.get('force'), nobackup=nobackup,
2730 2734 keepchanges=keepchanges)
2731 2735 else:
2732 2736 ret = q.push(repo, patch, force=opts.get('force'), nobackup=nobackup,
2733 2737 keepchanges=keepchanges)
2734 2738 q.savedirty()
2735 2739 return ret
2736 2740
2737 2741 @command("qguard",
2738 2742 [('l', 'list', None, _('list all patches and guards')),
2739 2743 ('n', 'none', None, _('drop all guards'))],
2740 2744 _('hg qguard [-l] [-n] [PATCH] [-- [+GUARD]... [-GUARD]...]'))
2741 2745 def guard(ui, repo, *args, **opts):
2742 2746 '''set or print guards for a patch
2743 2747
2744 2748 Guards control whether a patch can be pushed. A patch with no
2745 2749 guards is always pushed. A patch with a positive guard ("+foo") is
2746 2750 pushed only if the :hg:`qselect` command has activated it. A patch with
2747 2751 a negative guard ("-foo") is never pushed if the :hg:`qselect` command
2748 2752 has activated it.
2749 2753
2750 2754 With no arguments, print the currently active guards.
2751 2755 With arguments, set guards for the named patch.
2752 2756
2753 2757 .. note::
2754 2758
2755 2759 Specifying negative guards now requires '--'.
2756 2760
2757 2761 To set guards on another patch::
2758 2762
2759 2763 hg qguard other.patch -- +2.6.17 -stable
2760 2764
2761 2765 Returns 0 on success.
2762 2766 '''
2763 2767 def status(idx):
2764 2768 guards = q.seriesguards[idx] or ['unguarded']
2765 2769 if q.series[idx] in applied:
2766 2770 state = 'applied'
2767 2771 elif q.pushable(idx)[0]:
2768 2772 state = 'unapplied'
2769 2773 else:
2770 2774 state = 'guarded'
2771 2775 label = 'qguard.patch qguard.%s qseries.%s' % (state, state)
2772 2776 ui.write('%s: ' % ui.label(q.series[idx], label))
2773 2777
2774 2778 for i, guard in enumerate(guards):
2775 2779 if guard.startswith('+'):
2776 2780 ui.write(guard, label='qguard.positive')
2777 2781 elif guard.startswith('-'):
2778 2782 ui.write(guard, label='qguard.negative')
2779 2783 else:
2780 2784 ui.write(guard, label='qguard.unguarded')
2781 2785 if i != len(guards) - 1:
2782 2786 ui.write(' ')
2783 2787 ui.write('\n')
2784 2788 q = repo.mq
2785 2789 applied = set(p.name for p in q.applied)
2786 2790 patch = None
2787 2791 args = list(args)
2788 2792 if opts.get('list'):
2789 2793 if args or opts.get('none'):
2790 2794 raise util.Abort(_('cannot mix -l/--list with options or '
2791 2795 'arguments'))
2792 2796 for i in xrange(len(q.series)):
2793 2797 status(i)
2794 2798 return
2795 2799 if not args or args[0][0:1] in '-+':
2796 2800 if not q.applied:
2797 2801 raise util.Abort(_('no patches applied'))
2798 2802 patch = q.applied[-1].name
2799 2803 if patch is None and args[0][0:1] not in '-+':
2800 2804 patch = args.pop(0)
2801 2805 if patch is None:
2802 2806 raise util.Abort(_('no patch to work with'))
2803 2807 if args or opts.get('none'):
2804 2808 idx = q.findseries(patch)
2805 2809 if idx is None:
2806 2810 raise util.Abort(_('no patch named %s') % patch)
2807 2811 q.setguards(idx, args)
2808 2812 q.savedirty()
2809 2813 else:
2810 2814 status(q.series.index(q.lookup(patch)))
2811 2815
2812 2816 @command("qheader", [], _('hg qheader [PATCH]'))
2813 2817 def header(ui, repo, patch=None):
2814 2818 """print the header of the topmost or specified patch
2815 2819
2816 2820 Returns 0 on success."""
2817 2821 q = repo.mq
2818 2822
2819 2823 if patch:
2820 2824 patch = q.lookup(patch)
2821 2825 else:
2822 2826 if not q.applied:
2823 2827 ui.write(_('no patches applied\n'))
2824 2828 return 1
2825 2829 patch = q.lookup('qtip')
2826 2830 ph = patchheader(q.join(patch), q.plainmode)
2827 2831
2828 2832 ui.write('\n'.join(ph.message) + '\n')
2829 2833
2830 2834 def lastsavename(path):
2831 2835 (directory, base) = os.path.split(path)
2832 2836 names = os.listdir(directory)
2833 2837 namere = re.compile("%s.([0-9]+)" % base)
2834 2838 maxindex = None
2835 2839 maxname = None
2836 2840 for f in names:
2837 2841 m = namere.match(f)
2838 2842 if m:
2839 2843 index = int(m.group(1))
2840 2844 if maxindex is None or index > maxindex:
2841 2845 maxindex = index
2842 2846 maxname = f
2843 2847 if maxname:
2844 2848 return (os.path.join(directory, maxname), maxindex)
2845 2849 return (None, None)
2846 2850
2847 2851 def savename(path):
2848 2852 (last, index) = lastsavename(path)
2849 2853 if last is None:
2850 2854 index = 0
2851 2855 newpath = path + ".%d" % (index + 1)
2852 2856 return newpath
2853 2857
2854 2858 @command("^qpush",
2855 2859 [('', 'keep-changes', None,
2856 2860 _('tolerate non-conflicting local changes')),
2857 2861 ('f', 'force', None, _('apply on top of local changes')),
2858 2862 ('e', 'exact', None,
2859 2863 _('apply the target patch to its recorded parent')),
2860 2864 ('l', 'list', None, _('list patch name in commit text')),
2861 2865 ('a', 'all', None, _('apply all patches')),
2862 2866 ('m', 'merge', None, _('merge from another queue (DEPRECATED)')),
2863 2867 ('n', 'name', '',
2864 2868 _('merge queue name (DEPRECATED)'), _('NAME')),
2865 2869 ('', 'move', None,
2866 2870 _('reorder patch series and apply only the patch')),
2867 2871 ('', 'no-backup', None, _('do not save backup copies of files'))],
2868 2872 _('hg qpush [-f] [-l] [-a] [--move] [PATCH | INDEX]'))
2869 2873 def push(ui, repo, patch=None, **opts):
2870 2874 """push the next patch onto the stack
2871 2875
2872 2876 By default, abort if the working directory contains uncommitted
2873 2877 changes. With --keep-changes, abort only if the uncommitted files
2874 2878 overlap with patched files. With -f/--force, backup and patch over
2875 2879 uncommitted changes.
2876 2880
2877 2881 Return 0 on success.
2878 2882 """
2879 2883 q = repo.mq
2880 2884 mergeq = None
2881 2885
2882 2886 opts = fixkeepchangesopts(ui, opts)
2883 2887 if opts.get('merge'):
2884 2888 if opts.get('name'):
2885 2889 newpath = repo.join(opts.get('name'))
2886 2890 else:
2887 2891 newpath, i = lastsavename(q.path)
2888 2892 if not newpath:
2889 2893 ui.warn(_("no saved queues found, please use -n\n"))
2890 2894 return 1
2891 2895 mergeq = queue(ui, repo.baseui, repo.path, newpath)
2892 2896 ui.warn(_("merging with queue at: %s\n") % mergeq.path)
2893 2897 ret = q.push(repo, patch, force=opts.get('force'), list=opts.get('list'),
2894 2898 mergeq=mergeq, all=opts.get('all'), move=opts.get('move'),
2895 2899 exact=opts.get('exact'), nobackup=opts.get('no_backup'),
2896 2900 keepchanges=opts.get('keep_changes'))
2897 2901 return ret
2898 2902
2899 2903 @command("^qpop",
2900 2904 [('a', 'all', None, _('pop all patches')),
2901 2905 ('n', 'name', '',
2902 2906 _('queue name to pop (DEPRECATED)'), _('NAME')),
2903 2907 ('', 'keep-changes', None,
2904 2908 _('tolerate non-conflicting local changes')),
2905 2909 ('f', 'force', None, _('forget any local changes to patched files')),
2906 2910 ('', 'no-backup', None, _('do not save backup copies of files'))],
2907 2911 _('hg qpop [-a] [-f] [PATCH | INDEX]'))
2908 2912 def pop(ui, repo, patch=None, **opts):
2909 2913 """pop the current patch off the stack
2910 2914
2911 2915 Without argument, pops off the top of the patch stack. If given a
2912 2916 patch name, keeps popping off patches until the named patch is at
2913 2917 the top of the stack.
2914 2918
2915 2919 By default, abort if the working directory contains uncommitted
2916 2920 changes. With --keep-changes, abort only if the uncommitted files
2917 2921 overlap with patched files. With -f/--force, backup and discard
2918 2922 changes made to such files.
2919 2923
2920 2924 Return 0 on success.
2921 2925 """
2922 2926 opts = fixkeepchangesopts(ui, opts)
2923 2927 localupdate = True
2924 2928 if opts.get('name'):
2925 2929 q = queue(ui, repo.baseui, repo.path, repo.join(opts.get('name')))
2926 2930 ui.warn(_('using patch queue: %s\n') % q.path)
2927 2931 localupdate = False
2928 2932 else:
2929 2933 q = repo.mq
2930 2934 ret = q.pop(repo, patch, force=opts.get('force'), update=localupdate,
2931 2935 all=opts.get('all'), nobackup=opts.get('no_backup'),
2932 2936 keepchanges=opts.get('keep_changes'))
2933 2937 q.savedirty()
2934 2938 return ret
2935 2939
2936 2940 @command("qrename|qmv", [], _('hg qrename PATCH1 [PATCH2]'))
2937 2941 def rename(ui, repo, patch, name=None, **opts):
2938 2942 """rename a patch
2939 2943
2940 2944 With one argument, renames the current patch to PATCH1.
2941 2945 With two arguments, renames PATCH1 to PATCH2.
2942 2946
2943 2947 Returns 0 on success."""
2944 2948 q = repo.mq
2945 2949 if not name:
2946 2950 name = patch
2947 2951 patch = None
2948 2952
2949 2953 if patch:
2950 2954 patch = q.lookup(patch)
2951 2955 else:
2952 2956 if not q.applied:
2953 2957 ui.write(_('no patches applied\n'))
2954 2958 return
2955 2959 patch = q.lookup('qtip')
2956 2960 absdest = q.join(name)
2957 2961 if os.path.isdir(absdest):
2958 2962 name = normname(os.path.join(name, os.path.basename(patch)))
2959 2963 absdest = q.join(name)
2960 2964 q.checkpatchname(name)
2961 2965
2962 2966 ui.note(_('renaming %s to %s\n') % (patch, name))
2963 2967 i = q.findseries(patch)
2964 2968 guards = q.guard_re.findall(q.fullseries[i])
2965 2969 q.fullseries[i] = name + ''.join([' #' + g for g in guards])
2966 2970 q.parseseries()
2967 2971 q.seriesdirty = True
2968 2972
2969 2973 info = q.isapplied(patch)
2970 2974 if info:
2971 2975 q.applied[info[0]] = statusentry(info[1], name)
2972 2976 q.applieddirty = True
2973 2977
2974 2978 destdir = os.path.dirname(absdest)
2975 2979 if not os.path.isdir(destdir):
2976 2980 os.makedirs(destdir)
2977 2981 util.rename(q.join(patch), absdest)
2978 2982 r = q.qrepo()
2979 2983 if r and patch in r.dirstate:
2980 2984 wctx = r[None]
2981 2985 wlock = r.wlock()
2982 2986 try:
2983 2987 if r.dirstate[patch] == 'a':
2984 2988 r.dirstate.drop(patch)
2985 2989 r.dirstate.add(name)
2986 2990 else:
2987 2991 wctx.copy(patch, name)
2988 2992 wctx.forget([patch])
2989 2993 finally:
2990 2994 wlock.release()
2991 2995
2992 2996 q.savedirty()
2993 2997
2994 2998 @command("qrestore",
2995 2999 [('d', 'delete', None, _('delete save entry')),
2996 3000 ('u', 'update', None, _('update queue working directory'))],
2997 3001 _('hg qrestore [-d] [-u] REV'))
2998 3002 def restore(ui, repo, rev, **opts):
2999 3003 """restore the queue state saved by a revision (DEPRECATED)
3000 3004
3001 3005 This command is deprecated, use :hg:`rebase` instead."""
3002 3006 rev = repo.lookup(rev)
3003 3007 q = repo.mq
3004 3008 q.restore(repo, rev, delete=opts.get('delete'),
3005 3009 qupdate=opts.get('update'))
3006 3010 q.savedirty()
3007 3011 return 0
3008 3012
3009 3013 @command("qsave",
3010 3014 [('c', 'copy', None, _('copy patch directory')),
3011 3015 ('n', 'name', '',
3012 3016 _('copy directory name'), _('NAME')),
3013 3017 ('e', 'empty', None, _('clear queue status file')),
3014 3018 ('f', 'force', None, _('force copy'))] + commands.commitopts,
3015 3019 _('hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'))
3016 3020 def save(ui, repo, **opts):
3017 3021 """save current queue state (DEPRECATED)
3018 3022
3019 3023 This command is deprecated, use :hg:`rebase` instead."""
3020 3024 q = repo.mq
3021 3025 message = cmdutil.logmessage(ui, opts)
3022 3026 ret = q.save(repo, msg=message)
3023 3027 if ret:
3024 3028 return ret
3025 3029 q.savedirty() # save to .hg/patches before copying
3026 3030 if opts.get('copy'):
3027 3031 path = q.path
3028 3032 if opts.get('name'):
3029 3033 newpath = os.path.join(q.basepath, opts.get('name'))
3030 3034 if os.path.exists(newpath):
3031 3035 if not os.path.isdir(newpath):
3032 3036 raise util.Abort(_('destination %s exists and is not '
3033 3037 'a directory') % newpath)
3034 3038 if not opts.get('force'):
3035 3039 raise util.Abort(_('destination %s exists, '
3036 3040 'use -f to force') % newpath)
3037 3041 else:
3038 3042 newpath = savename(path)
3039 3043 ui.warn(_("copy %s to %s\n") % (path, newpath))
3040 3044 util.copyfiles(path, newpath)
3041 3045 if opts.get('empty'):
3042 3046 del q.applied[:]
3043 3047 q.applieddirty = True
3044 3048 q.savedirty()
3045 3049 return 0
3046 3050
3047 3051
3048 3052 @command("qselect",
3049 3053 [('n', 'none', None, _('disable all guards')),
3050 3054 ('s', 'series', None, _('list all guards in series file')),
3051 3055 ('', 'pop', None, _('pop to before first guarded applied patch')),
3052 3056 ('', 'reapply', None, _('pop, then reapply patches'))],
3053 3057 _('hg qselect [OPTION]... [GUARD]...'))
3054 3058 def select(ui, repo, *args, **opts):
3055 3059 '''set or print guarded patches to push
3056 3060
3057 3061 Use the :hg:`qguard` command to set or print guards on patch, then use
3058 3062 qselect to tell mq which guards to use. A patch will be pushed if
3059 3063 it has no guards or any positive guards match the currently
3060 3064 selected guard, but will not be pushed if any negative guards
3061 3065 match the current guard. For example::
3062 3066
3063 3067 qguard foo.patch -- -stable (negative guard)
3064 3068 qguard bar.patch +stable (positive guard)
3065 3069 qselect stable
3066 3070
3067 3071 This activates the "stable" guard. mq will skip foo.patch (because
3068 3072 it has a negative match) but push bar.patch (because it has a
3069 3073 positive match).
3070 3074
3071 3075 With no arguments, prints the currently active guards.
3072 3076 With one argument, sets the active guard.
3073 3077
3074 3078 Use -n/--none to deactivate guards (no other arguments needed).
3075 3079 When no guards are active, patches with positive guards are
3076 3080 skipped and patches with negative guards are pushed.
3077 3081
3078 3082 qselect can change the guards on applied patches. It does not pop
3079 3083 guarded patches by default. Use --pop to pop back to the last
3080 3084 applied patch that is not guarded. Use --reapply (which implies
3081 3085 --pop) to push back to the current patch afterwards, but skip
3082 3086 guarded patches.
3083 3087
3084 3088 Use -s/--series to print a list of all guards in the series file
3085 3089 (no other arguments needed). Use -v for more information.
3086 3090
3087 3091 Returns 0 on success.'''
3088 3092
3089 3093 q = repo.mq
3090 3094 guards = q.active()
3091 3095 pushable = lambda i: q.pushable(q.applied[i].name)[0]
3092 3096 if args or opts.get('none'):
3093 3097 old_unapplied = q.unapplied(repo)
3094 3098 old_guarded = [i for i in xrange(len(q.applied)) if not pushable(i)]
3095 3099 q.setactive(args)
3096 3100 q.savedirty()
3097 3101 if not args:
3098 3102 ui.status(_('guards deactivated\n'))
3099 3103 if not opts.get('pop') and not opts.get('reapply'):
3100 3104 unapplied = q.unapplied(repo)
3101 3105 guarded = [i for i in xrange(len(q.applied)) if not pushable(i)]
3102 3106 if len(unapplied) != len(old_unapplied):
3103 3107 ui.status(_('number of unguarded, unapplied patches has '
3104 3108 'changed from %d to %d\n') %
3105 3109 (len(old_unapplied), len(unapplied)))
3106 3110 if len(guarded) != len(old_guarded):
3107 3111 ui.status(_('number of guarded, applied patches has changed '
3108 3112 'from %d to %d\n') %
3109 3113 (len(old_guarded), len(guarded)))
3110 3114 elif opts.get('series'):
3111 3115 guards = {}
3112 3116 noguards = 0
3113 3117 for gs in q.seriesguards:
3114 3118 if not gs:
3115 3119 noguards += 1
3116 3120 for g in gs:
3117 3121 guards.setdefault(g, 0)
3118 3122 guards[g] += 1
3119 3123 if ui.verbose:
3120 3124 guards['NONE'] = noguards
3121 3125 guards = guards.items()
3122 3126 guards.sort(key=lambda x: x[0][1:])
3123 3127 if guards:
3124 3128 ui.note(_('guards in series file:\n'))
3125 3129 for guard, count in guards:
3126 3130 ui.note('%2d ' % count)
3127 3131 ui.write(guard, '\n')
3128 3132 else:
3129 3133 ui.note(_('no guards in series file\n'))
3130 3134 else:
3131 3135 if guards:
3132 3136 ui.note(_('active guards:\n'))
3133 3137 for g in guards:
3134 3138 ui.write(g, '\n')
3135 3139 else:
3136 3140 ui.write(_('no active guards\n'))
3137 3141 reapply = opts.get('reapply') and q.applied and q.applied[-1].name
3138 3142 popped = False
3139 3143 if opts.get('pop') or opts.get('reapply'):
3140 3144 for i in xrange(len(q.applied)):
3141 3145 if not pushable(i):
3142 3146 ui.status(_('popping guarded patches\n'))
3143 3147 popped = True
3144 3148 if i == 0:
3145 3149 q.pop(repo, all=True)
3146 3150 else:
3147 3151 q.pop(repo, q.applied[i - 1].name)
3148 3152 break
3149 3153 if popped:
3150 3154 try:
3151 3155 if reapply:
3152 3156 ui.status(_('reapplying unguarded patches\n'))
3153 3157 q.push(repo, reapply)
3154 3158 finally:
3155 3159 q.savedirty()
3156 3160
3157 3161 @command("qfinish",
3158 3162 [('a', 'applied', None, _('finish all applied changesets'))],
3159 3163 _('hg qfinish [-a] [REV]...'))
3160 3164 def finish(ui, repo, *revrange, **opts):
3161 3165 """move applied patches into repository history
3162 3166
3163 3167 Finishes the specified revisions (corresponding to applied
3164 3168 patches) by moving them out of mq control into regular repository
3165 3169 history.
3166 3170
3167 3171 Accepts a revision range or the -a/--applied option. If --applied
3168 3172 is specified, all applied mq revisions are removed from mq
3169 3173 control. Otherwise, the given revisions must be at the base of the
3170 3174 stack of applied patches.
3171 3175
3172 3176 This can be especially useful if your changes have been applied to
3173 3177 an upstream repository, or if you are about to push your changes
3174 3178 to upstream.
3175 3179
3176 3180 Returns 0 on success.
3177 3181 """
3178 3182 if not opts.get('applied') and not revrange:
3179 3183 raise util.Abort(_('no revisions specified'))
3180 3184 elif opts.get('applied'):
3181 3185 revrange = ('qbase::qtip',) + revrange
3182 3186
3183 3187 q = repo.mq
3184 3188 if not q.applied:
3185 3189 ui.status(_('no patches applied\n'))
3186 3190 return 0
3187 3191
3188 3192 revs = scmutil.revrange(repo, revrange)
3189 3193 if repo['.'].rev() in revs and repo[None].files():
3190 3194 ui.warn(_('warning: uncommitted changes in the working directory\n'))
3191 3195 # queue.finish may changes phases but leave the responsibility to lock the
3192 3196 # repo to the caller to avoid deadlock with wlock. This command code is
3193 3197 # responsibility for this locking.
3194 3198 lock = repo.lock()
3195 3199 try:
3196 3200 q.finish(repo, revs)
3197 3201 q.savedirty()
3198 3202 finally:
3199 3203 lock.release()
3200 3204 return 0
3201 3205
3202 3206 @command("qqueue",
3203 3207 [('l', 'list', False, _('list all available queues')),
3204 3208 ('', 'active', False, _('print name of active queue')),
3205 3209 ('c', 'create', False, _('create new queue')),
3206 3210 ('', 'rename', False, _('rename active queue')),
3207 3211 ('', 'delete', False, _('delete reference to queue')),
3208 3212 ('', 'purge', False, _('delete queue, and remove patch dir')),
3209 3213 ],
3210 3214 _('[OPTION] [QUEUE]'))
3211 3215 def qqueue(ui, repo, name=None, **opts):
3212 3216 '''manage multiple patch queues
3213 3217
3214 3218 Supports switching between different patch queues, as well as creating
3215 3219 new patch queues and deleting existing ones.
3216 3220
3217 3221 Omitting a queue name or specifying -l/--list will show you the registered
3218 3222 queues - by default the "normal" patches queue is registered. The currently
3219 3223 active queue will be marked with "(active)". Specifying --active will print
3220 3224 only the name of the active queue.
3221 3225
3222 3226 To create a new queue, use -c/--create. The queue is automatically made
3223 3227 active, except in the case where there are applied patches from the
3224 3228 currently active queue in the repository. Then the queue will only be
3225 3229 created and switching will fail.
3226 3230
3227 3231 To delete an existing queue, use --delete. You cannot delete the currently
3228 3232 active queue.
3229 3233
3230 3234 Returns 0 on success.
3231 3235 '''
3232 3236 q = repo.mq
3233 3237 _defaultqueue = 'patches'
3234 3238 _allqueues = 'patches.queues'
3235 3239 _activequeue = 'patches.queue'
3236 3240
3237 3241 def _getcurrent():
3238 3242 cur = os.path.basename(q.path)
3239 3243 if cur.startswith('patches-'):
3240 3244 cur = cur[8:]
3241 3245 return cur
3242 3246
3243 3247 def _noqueues():
3244 3248 try:
3245 3249 fh = repo.vfs(_allqueues, 'r')
3246 3250 fh.close()
3247 3251 except IOError:
3248 3252 return True
3249 3253
3250 3254 return False
3251 3255
3252 3256 def _getqueues():
3253 3257 current = _getcurrent()
3254 3258
3255 3259 try:
3256 3260 fh = repo.vfs(_allqueues, 'r')
3257 3261 queues = [queue.strip() for queue in fh if queue.strip()]
3258 3262 fh.close()
3259 3263 if current not in queues:
3260 3264 queues.append(current)
3261 3265 except IOError:
3262 3266 queues = [_defaultqueue]
3263 3267
3264 3268 return sorted(queues)
3265 3269
3266 3270 def _setactive(name):
3267 3271 if q.applied:
3268 3272 raise util.Abort(_('new queue created, but cannot make active '
3269 3273 'as patches are applied'))
3270 3274 _setactivenocheck(name)
3271 3275
3272 3276 def _setactivenocheck(name):
3273 3277 fh = repo.vfs(_activequeue, 'w')
3274 3278 if name != 'patches':
3275 3279 fh.write(name)
3276 3280 fh.close()
3277 3281
3278 3282 def _addqueue(name):
3279 3283 fh = repo.vfs(_allqueues, 'a')
3280 3284 fh.write('%s\n' % (name,))
3281 3285 fh.close()
3282 3286
3283 3287 def _queuedir(name):
3284 3288 if name == 'patches':
3285 3289 return repo.join('patches')
3286 3290 else:
3287 3291 return repo.join('patches-' + name)
3288 3292
3289 3293 def _validname(name):
3290 3294 for n in name:
3291 3295 if n in ':\\/.':
3292 3296 return False
3293 3297 return True
3294 3298
3295 3299 def _delete(name):
3296 3300 if name not in existing:
3297 3301 raise util.Abort(_('cannot delete queue that does not exist'))
3298 3302
3299 3303 current = _getcurrent()
3300 3304
3301 3305 if name == current:
3302 3306 raise util.Abort(_('cannot delete currently active queue'))
3303 3307
3304 3308 fh = repo.vfs('patches.queues.new', 'w')
3305 3309 for queue in existing:
3306 3310 if queue == name:
3307 3311 continue
3308 3312 fh.write('%s\n' % (queue,))
3309 3313 fh.close()
3310 3314 util.rename(repo.join('patches.queues.new'), repo.join(_allqueues))
3311 3315
3312 3316 if not name or opts.get('list') or opts.get('active'):
3313 3317 current = _getcurrent()
3314 3318 if opts.get('active'):
3315 3319 ui.write('%s\n' % (current,))
3316 3320 return
3317 3321 for queue in _getqueues():
3318 3322 ui.write('%s' % (queue,))
3319 3323 if queue == current and not ui.quiet:
3320 3324 ui.write(_(' (active)\n'))
3321 3325 else:
3322 3326 ui.write('\n')
3323 3327 return
3324 3328
3325 3329 if not _validname(name):
3326 3330 raise util.Abort(
3327 3331 _('invalid queue name, may not contain the characters ":\\/."'))
3328 3332
3329 3333 existing = _getqueues()
3330 3334
3331 3335 if opts.get('create'):
3332 3336 if name in existing:
3333 3337 raise util.Abort(_('queue "%s" already exists') % name)
3334 3338 if _noqueues():
3335 3339 _addqueue(_defaultqueue)
3336 3340 _addqueue(name)
3337 3341 _setactive(name)
3338 3342 elif opts.get('rename'):
3339 3343 current = _getcurrent()
3340 3344 if name == current:
3341 3345 raise util.Abort(_('can\'t rename "%s" to its current name') % name)
3342 3346 if name in existing:
3343 3347 raise util.Abort(_('queue "%s" already exists') % name)
3344 3348
3345 3349 olddir = _queuedir(current)
3346 3350 newdir = _queuedir(name)
3347 3351
3348 3352 if os.path.exists(newdir):
3349 3353 raise util.Abort(_('non-queue directory "%s" already exists') %
3350 3354 newdir)
3351 3355
3352 3356 fh = repo.vfs('patches.queues.new', 'w')
3353 3357 for queue in existing:
3354 3358 if queue == current:
3355 3359 fh.write('%s\n' % (name,))
3356 3360 if os.path.exists(olddir):
3357 3361 util.rename(olddir, newdir)
3358 3362 else:
3359 3363 fh.write('%s\n' % (queue,))
3360 3364 fh.close()
3361 3365 util.rename(repo.join('patches.queues.new'), repo.join(_allqueues))
3362 3366 _setactivenocheck(name)
3363 3367 elif opts.get('delete'):
3364 3368 _delete(name)
3365 3369 elif opts.get('purge'):
3366 3370 if name in existing:
3367 3371 _delete(name)
3368 3372 qdir = _queuedir(name)
3369 3373 if os.path.exists(qdir):
3370 3374 shutil.rmtree(qdir)
3371 3375 else:
3372 3376 if name not in existing:
3373 3377 raise util.Abort(_('use --create to create a new queue'))
3374 3378 _setactive(name)
3375 3379
3376 3380 def mqphasedefaults(repo, roots):
3377 3381 """callback used to set mq changeset as secret when no phase data exists"""
3378 3382 if repo.mq.applied:
3379 3383 if repo.ui.configbool('mq', 'secret', False):
3380 3384 mqphase = phases.secret
3381 3385 else:
3382 3386 mqphase = phases.draft
3383 3387 qbase = repo[repo.mq.applied[0].node]
3384 3388 roots[mqphase].add(qbase.node())
3385 3389 return roots
3386 3390
3387 3391 def reposetup(ui, repo):
3388 3392 class mqrepo(repo.__class__):
3389 3393 @localrepo.unfilteredpropertycache
3390 3394 def mq(self):
3391 3395 return queue(self.ui, self.baseui, self.path)
3392 3396
3393 3397 def invalidateall(self):
3394 3398 super(mqrepo, self).invalidateall()
3395 3399 if localrepo.hasunfilteredcache(self, 'mq'):
3396 3400 # recreate mq in case queue path was changed
3397 3401 delattr(self.unfiltered(), 'mq')
3398 3402
3399 3403 def abortifwdirpatched(self, errmsg, force=False):
3400 3404 if self.mq.applied and self.mq.checkapplied and not force:
3401 3405 parents = self.dirstate.parents()
3402 3406 patches = [s.node for s in self.mq.applied]
3403 3407 if parents[0] in patches or parents[1] in patches:
3404 3408 raise util.Abort(errmsg)
3405 3409
3406 3410 def commit(self, text="", user=None, date=None, match=None,
3407 3411 force=False, editor=False, extra={}):
3408 3412 self.abortifwdirpatched(
3409 3413 _('cannot commit over an applied mq patch'),
3410 3414 force)
3411 3415
3412 3416 return super(mqrepo, self).commit(text, user, date, match, force,
3413 3417 editor, extra)
3414 3418
3415 3419 def checkpush(self, pushop):
3416 3420 if self.mq.applied and self.mq.checkapplied and not pushop.force:
3417 3421 outapplied = [e.node for e in self.mq.applied]
3418 3422 if pushop.revs:
3419 3423 # Assume applied patches have no non-patch descendants and
3420 3424 # are not on remote already. Filtering any changeset not
3421 3425 # pushed.
3422 3426 heads = set(pushop.revs)
3423 3427 for node in reversed(outapplied):
3424 3428 if node in heads:
3425 3429 break
3426 3430 else:
3427 3431 outapplied.pop()
3428 3432 # looking for pushed and shared changeset
3429 3433 for node in outapplied:
3430 3434 if self[node].phase() < phases.secret:
3431 3435 raise util.Abort(_('source has mq patches applied'))
3432 3436 # no non-secret patches pushed
3433 3437 super(mqrepo, self).checkpush(pushop)
3434 3438
3435 3439 def _findtags(self):
3436 3440 '''augment tags from base class with patch tags'''
3437 3441 result = super(mqrepo, self)._findtags()
3438 3442
3439 3443 q = self.mq
3440 3444 if not q.applied:
3441 3445 return result
3442 3446
3443 3447 mqtags = [(patch.node, patch.name) for patch in q.applied]
3444 3448
3445 3449 try:
3446 3450 # for now ignore filtering business
3447 3451 self.unfiltered().changelog.rev(mqtags[-1][0])
3448 3452 except error.LookupError:
3449 3453 self.ui.warn(_('mq status file refers to unknown node %s\n')
3450 3454 % short(mqtags[-1][0]))
3451 3455 return result
3452 3456
3453 3457 # do not add fake tags for filtered revisions
3454 3458 included = self.changelog.hasnode
3455 3459 mqtags = [mqt for mqt in mqtags if included(mqt[0])]
3456 3460 if not mqtags:
3457 3461 return result
3458 3462
3459 3463 mqtags.append((mqtags[-1][0], 'qtip'))
3460 3464 mqtags.append((mqtags[0][0], 'qbase'))
3461 3465 mqtags.append((self.changelog.parents(mqtags[0][0])[0], 'qparent'))
3462 3466 tags = result[0]
3463 3467 for patch in mqtags:
3464 3468 if patch[1] in tags:
3465 3469 self.ui.warn(_('tag %s overrides mq patch of the same '
3466 3470 'name\n') % patch[1])
3467 3471 else:
3468 3472 tags[patch[1]] = patch[0]
3469 3473
3470 3474 return result
3471 3475
3472 3476 if repo.local():
3473 3477 repo.__class__ = mqrepo
3474 3478
3475 3479 repo._phasedefaults.append(mqphasedefaults)
3476 3480
3477 3481 def mqimport(orig, ui, repo, *args, **kwargs):
3478 3482 if (util.safehasattr(repo, 'abortifwdirpatched')
3479 3483 and not kwargs.get('no_commit', False)):
3480 3484 repo.abortifwdirpatched(_('cannot import over an applied patch'),
3481 3485 kwargs.get('force'))
3482 3486 return orig(ui, repo, *args, **kwargs)
3483 3487
3484 3488 def mqinit(orig, ui, *args, **kwargs):
3485 3489 mq = kwargs.pop('mq', None)
3486 3490
3487 3491 if not mq:
3488 3492 return orig(ui, *args, **kwargs)
3489 3493
3490 3494 if args:
3491 3495 repopath = args[0]
3492 3496 if not hg.islocal(repopath):
3493 3497 raise util.Abort(_('only a local queue repository '
3494 3498 'may be initialized'))
3495 3499 else:
3496 3500 repopath = cmdutil.findrepo(os.getcwd())
3497 3501 if not repopath:
3498 3502 raise util.Abort(_('there is no Mercurial repository here '
3499 3503 '(.hg not found)'))
3500 3504 repo = hg.repository(ui, repopath)
3501 3505 return qinit(ui, repo, True)
3502 3506
3503 3507 def mqcommand(orig, ui, repo, *args, **kwargs):
3504 3508 """Add --mq option to operate on patch repository instead of main"""
3505 3509
3506 3510 # some commands do not like getting unknown options
3507 3511 mq = kwargs.pop('mq', None)
3508 3512
3509 3513 if not mq:
3510 3514 return orig(ui, repo, *args, **kwargs)
3511 3515
3512 3516 q = repo.mq
3513 3517 r = q.qrepo()
3514 3518 if not r:
3515 3519 raise util.Abort(_('no queue repository'))
3516 3520 return orig(r.ui, r, *args, **kwargs)
3517 3521
3518 3522 def summaryhook(ui, repo):
3519 3523 q = repo.mq
3520 3524 m = []
3521 3525 a, u = len(q.applied), len(q.unapplied(repo))
3522 3526 if a:
3523 3527 m.append(ui.label(_("%d applied"), 'qseries.applied') % a)
3524 3528 if u:
3525 3529 m.append(ui.label(_("%d unapplied"), 'qseries.unapplied') % u)
3526 3530 if m:
3527 3531 # i18n: column positioning for "hg summary"
3528 3532 ui.write(_("mq: %s\n") % ', '.join(m))
3529 3533 else:
3530 3534 # i18n: column positioning for "hg summary"
3531 3535 ui.note(_("mq: (empty queue)\n"))
3532 3536
3533 3537 def revsetmq(repo, subset, x):
3534 3538 """``mq()``
3535 3539 Changesets managed by MQ.
3536 3540 """
3537 3541 revset.getargs(x, 0, 0, _("mq takes no arguments"))
3538 3542 applied = set([repo[r.node].rev() for r in repo.mq.applied])
3539 3543 return revset.baseset([r for r in subset if r in applied])
3540 3544
3541 3545 # tell hggettext to extract docstrings from these functions:
3542 3546 i18nfunctions = [revsetmq]
3543 3547
3544 3548 def extsetup(ui):
3545 3549 # Ensure mq wrappers are called first, regardless of extension load order by
3546 3550 # NOT wrapping in uisetup() and instead deferring to init stage two here.
3547 3551 mqopt = [('', 'mq', None, _("operate on patch repository"))]
3548 3552
3549 3553 extensions.wrapcommand(commands.table, 'import', mqimport)
3550 3554 cmdutil.summaryhooks.add('mq', summaryhook)
3551 3555
3552 3556 entry = extensions.wrapcommand(commands.table, 'init', mqinit)
3553 3557 entry[1].extend(mqopt)
3554 3558
3555 3559 nowrap = set(commands.norepo.split(" "))
3556 3560
3557 3561 def dotable(cmdtable):
3558 3562 for cmd in cmdtable.keys():
3559 3563 cmd = cmdutil.parsealiases(cmd)[0]
3560 3564 if cmd in nowrap:
3561 3565 continue
3562 3566 entry = extensions.wrapcommand(cmdtable, cmd, mqcommand)
3563 3567 entry[1].extend(mqopt)
3564 3568
3565 3569 dotable(commands.table)
3566 3570
3567 3571 for extname, extmodule in extensions.extensions():
3568 3572 if extmodule.__file__ != __file__:
3569 3573 dotable(getattr(extmodule, 'cmdtable', {}))
3570 3574
3571 3575 revset.symbols['mq'] = revsetmq
3572 3576
3573 3577 colortable = {'qguard.negative': 'red',
3574 3578 'qguard.positive': 'yellow',
3575 3579 'qguard.unguarded': 'green',
3576 3580 'qseries.applied': 'blue bold underline',
3577 3581 'qseries.guarded': 'black bold',
3578 3582 'qseries.missing': 'red bold',
3579 3583 'qseries.unapplied': 'black bold'}
@@ -1,415 +1,419 b''
1 1 # notify.py - email notifications 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 '''hooks for sending email push notifications
9 9
10 10 This extension implements hooks to send email notifications when
11 11 changesets are sent from or received by the local repository.
12 12
13 13 First, enable the extension as explained in :hg:`help extensions`, and
14 14 register the hook you want to run. ``incoming`` and ``changegroup`` hooks
15 15 are run when changesets are received, while ``outgoing`` hooks are for
16 16 changesets sent to another repository::
17 17
18 18 [hooks]
19 19 # one email for each incoming changeset
20 20 incoming.notify = python:hgext.notify.hook
21 21 # one email for all incoming changesets
22 22 changegroup.notify = python:hgext.notify.hook
23 23
24 24 # one email for all outgoing changesets
25 25 outgoing.notify = python:hgext.notify.hook
26 26
27 27 This registers the hooks. To enable notification, subscribers must
28 28 be assigned to repositories. The ``[usersubs]`` section maps multiple
29 29 repositories to a given recipient. The ``[reposubs]`` section maps
30 30 multiple recipients to a single repository::
31 31
32 32 [usersubs]
33 33 # key is subscriber email, value is a comma-separated list of repo patterns
34 34 user@host = pattern
35 35
36 36 [reposubs]
37 37 # key is repo pattern, value is a comma-separated list of subscriber emails
38 38 pattern = user@host
39 39
40 40 A ``pattern`` is a ``glob`` matching the absolute path to a repository,
41 41 optionally combined with a revset expression. A revset expression, if
42 42 present, is separated from the glob by a hash. Example::
43 43
44 44 [reposubs]
45 45 */widgets#branch(release) = qa-team@example.com
46 46
47 47 This sends to ``qa-team@example.com`` whenever a changeset on the ``release``
48 48 branch triggers a notification in any repository ending in ``widgets``.
49 49
50 50 In order to place them under direct user management, ``[usersubs]`` and
51 51 ``[reposubs]`` sections may be placed in a separate ``hgrc`` file and
52 52 incorporated by reference::
53 53
54 54 [notify]
55 55 config = /path/to/subscriptionsfile
56 56
57 57 Notifications will not be sent until the ``notify.test`` value is set
58 58 to ``False``; see below.
59 59
60 60 Notifications content can be tweaked with the following configuration entries:
61 61
62 62 notify.test
63 63 If ``True``, print messages to stdout instead of sending them. Default: True.
64 64
65 65 notify.sources
66 66 Space-separated list of change sources. Notifications are activated only
67 67 when a changeset's source is in this list. Sources may be:
68 68
69 69 :``serve``: changesets received via http or ssh
70 70 :``pull``: changesets received via ``hg pull``
71 71 :``unbundle``: changesets received via ``hg unbundle``
72 72 :``push``: changesets sent or received via ``hg push``
73 73 :``bundle``: changesets sent via ``hg unbundle``
74 74
75 75 Default: serve.
76 76
77 77 notify.strip
78 78 Number of leading slashes to strip from url paths. By default, notifications
79 79 reference repositories with their absolute path. ``notify.strip`` lets you
80 80 turn them into relative paths. For example, ``notify.strip=3`` will change
81 81 ``/long/path/repository`` into ``repository``. Default: 0.
82 82
83 83 notify.domain
84 84 Default email domain for sender or recipients with no explicit domain.
85 85
86 86 notify.style
87 87 Style file to use when formatting emails.
88 88
89 89 notify.template
90 90 Template to use when formatting emails.
91 91
92 92 notify.incoming
93 93 Template to use when run as an incoming hook, overriding ``notify.template``.
94 94
95 95 notify.outgoing
96 96 Template to use when run as an outgoing hook, overriding ``notify.template``.
97 97
98 98 notify.changegroup
99 99 Template to use when running as a changegroup hook, overriding
100 100 ``notify.template``.
101 101
102 102 notify.maxdiff
103 103 Maximum number of diff lines to include in notification email. Set to 0
104 104 to disable the diff, or -1 to include all of it. Default: 300.
105 105
106 106 notify.maxsubject
107 107 Maximum number of characters in email's subject line. Default: 67.
108 108
109 109 notify.diffstat
110 110 Set to True to include a diffstat before diff content. Default: True.
111 111
112 112 notify.merge
113 113 If True, send notifications for merge changesets. Default: True.
114 114
115 115 notify.mbox
116 116 If set, append mails to this mbox file instead of sending. Default: None.
117 117
118 118 notify.fromauthor
119 119 If set, use the committer of the first changeset in a changegroup for
120 120 the "From" field of the notification mail. If not set, take the user
121 121 from the pushing repo. Default: False.
122 122
123 123 If set, the following entries will also be used to customize the
124 124 notifications:
125 125
126 126 email.from
127 127 Email ``From`` address to use if none can be found in the generated
128 128 email content.
129 129
130 130 web.baseurl
131 131 Root repository URL to combine with repository paths when making
132 132 references. See also ``notify.strip``.
133 133
134 134 '''
135 135
136 136 import email, socket, time
137 137 # On python2.4 you have to import this by name or they fail to
138 138 # load. This was not a problem on Python 2.7.
139 139 import email.Parser
140 140 from mercurial.i18n import _
141 141 from mercurial import patch, cmdutil, util, mail
142 142 import fnmatch
143 143
144 # Note for extension authors: ONLY specify testedwith = 'internal' for
145 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
146 # be specifying the version(s) of Mercurial they are tested with, or
147 # leave the attribute unspecified.
144 148 testedwith = 'internal'
145 149
146 150 # template for single changeset can include email headers.
147 151 single_template = '''
148 152 Subject: changeset in {webroot}: {desc|firstline|strip}
149 153 From: {author}
150 154
151 155 changeset {node|short} in {root}
152 156 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
153 157 description:
154 158 \t{desc|tabindent|strip}
155 159 '''.lstrip()
156 160
157 161 # template for multiple changesets should not contain email headers,
158 162 # because only first set of headers will be used and result will look
159 163 # strange.
160 164 multiple_template = '''
161 165 changeset {node|short} in {root}
162 166 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
163 167 summary: {desc|firstline}
164 168 '''
165 169
166 170 deftemplates = {
167 171 'changegroup': multiple_template,
168 172 }
169 173
170 174 class notifier(object):
171 175 '''email notification class.'''
172 176
173 177 def __init__(self, ui, repo, hooktype):
174 178 self.ui = ui
175 179 cfg = self.ui.config('notify', 'config')
176 180 if cfg:
177 181 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
178 182 self.repo = repo
179 183 self.stripcount = int(self.ui.config('notify', 'strip', 0))
180 184 self.root = self.strip(self.repo.root)
181 185 self.domain = self.ui.config('notify', 'domain')
182 186 self.mbox = self.ui.config('notify', 'mbox')
183 187 self.test = self.ui.configbool('notify', 'test', True)
184 188 self.charsets = mail._charsets(self.ui)
185 189 self.subs = self.subscribers()
186 190 self.merge = self.ui.configbool('notify', 'merge', True)
187 191
188 192 mapfile = self.ui.config('notify', 'style')
189 193 template = (self.ui.config('notify', hooktype) or
190 194 self.ui.config('notify', 'template'))
191 195 if not mapfile and not template:
192 196 template = deftemplates.get(hooktype) or single_template
193 197 self.t = cmdutil.changeset_templater(self.ui, self.repo, False, None,
194 198 template, mapfile, False)
195 199
196 200 def strip(self, path):
197 201 '''strip leading slashes from local path, turn into web-safe path.'''
198 202
199 203 path = util.pconvert(path)
200 204 count = self.stripcount
201 205 while count > 0:
202 206 c = path.find('/')
203 207 if c == -1:
204 208 break
205 209 path = path[c + 1:]
206 210 count -= 1
207 211 return path
208 212
209 213 def fixmail(self, addr):
210 214 '''try to clean up email addresses.'''
211 215
212 216 addr = util.email(addr.strip())
213 217 if self.domain:
214 218 a = addr.find('@localhost')
215 219 if a != -1:
216 220 addr = addr[:a]
217 221 if '@' not in addr:
218 222 return addr + '@' + self.domain
219 223 return addr
220 224
221 225 def subscribers(self):
222 226 '''return list of email addresses of subscribers to this repo.'''
223 227 subs = set()
224 228 for user, pats in self.ui.configitems('usersubs'):
225 229 for pat in pats.split(','):
226 230 if '#' in pat:
227 231 pat, revs = pat.split('#', 1)
228 232 else:
229 233 revs = None
230 234 if fnmatch.fnmatch(self.repo.root, pat.strip()):
231 235 subs.add((self.fixmail(user), revs))
232 236 for pat, users in self.ui.configitems('reposubs'):
233 237 if '#' in pat:
234 238 pat, revs = pat.split('#', 1)
235 239 else:
236 240 revs = None
237 241 if fnmatch.fnmatch(self.repo.root, pat):
238 242 for user in users.split(','):
239 243 subs.add((self.fixmail(user), revs))
240 244 return [(mail.addressencode(self.ui, s, self.charsets, self.test), r)
241 245 for s, r in sorted(subs)]
242 246
243 247 def node(self, ctx, **props):
244 248 '''format one changeset, unless it is a suppressed merge.'''
245 249 if not self.merge and len(ctx.parents()) > 1:
246 250 return False
247 251 self.t.show(ctx, changes=ctx.changeset(),
248 252 baseurl=self.ui.config('web', 'baseurl'),
249 253 root=self.repo.root, webroot=self.root, **props)
250 254 return True
251 255
252 256 def skipsource(self, source):
253 257 '''true if incoming changes from this source should be skipped.'''
254 258 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
255 259 return source not in ok_sources
256 260
257 261 def send(self, ctx, count, data):
258 262 '''send message.'''
259 263
260 264 # Select subscribers by revset
261 265 subs = set()
262 266 for sub, spec in self.subs:
263 267 if spec is None:
264 268 subs.add(sub)
265 269 continue
266 270 revs = self.repo.revs('%r and %d:', spec, ctx.rev())
267 271 if len(revs):
268 272 subs.add(sub)
269 273 continue
270 274 if len(subs) == 0:
271 275 self.ui.debug('notify: no subscribers to selected repo '
272 276 'and revset\n')
273 277 return
274 278
275 279 p = email.Parser.Parser()
276 280 try:
277 281 msg = p.parsestr(data)
278 282 except email.Errors.MessageParseError, inst:
279 283 raise util.Abort(inst)
280 284
281 285 # store sender and subject
282 286 sender, subject = msg['From'], msg['Subject']
283 287 del msg['From'], msg['Subject']
284 288
285 289 if not msg.is_multipart():
286 290 # create fresh mime message from scratch
287 291 # (multipart templates must take care of this themselves)
288 292 headers = msg.items()
289 293 payload = msg.get_payload()
290 294 # for notification prefer readability over data precision
291 295 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
292 296 # reinstate custom headers
293 297 for k, v in headers:
294 298 msg[k] = v
295 299
296 300 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
297 301
298 302 # try to make subject line exist and be useful
299 303 if not subject:
300 304 if count > 1:
301 305 subject = _('%s: %d new changesets') % (self.root, count)
302 306 else:
303 307 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
304 308 subject = '%s: %s' % (self.root, s)
305 309 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
306 310 if maxsubject:
307 311 subject = util.ellipsis(subject, maxsubject)
308 312 msg['Subject'] = mail.headencode(self.ui, subject,
309 313 self.charsets, self.test)
310 314
311 315 # try to make message have proper sender
312 316 if not sender:
313 317 sender = self.ui.config('email', 'from') or self.ui.username()
314 318 if '@' not in sender or '@localhost' in sender:
315 319 sender = self.fixmail(sender)
316 320 msg['From'] = mail.addressencode(self.ui, sender,
317 321 self.charsets, self.test)
318 322
319 323 msg['X-Hg-Notification'] = 'changeset %s' % ctx
320 324 if not msg['Message-Id']:
321 325 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
322 326 (ctx, int(time.time()),
323 327 hash(self.repo.root), socket.getfqdn()))
324 328 msg['To'] = ', '.join(sorted(subs))
325 329
326 330 msgtext = msg.as_string()
327 331 if self.test:
328 332 self.ui.write(msgtext)
329 333 if not msgtext.endswith('\n'):
330 334 self.ui.write('\n')
331 335 else:
332 336 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
333 337 (len(subs), count))
334 338 mail.sendmail(self.ui, util.email(msg['From']),
335 339 subs, msgtext, mbox=self.mbox)
336 340
337 341 def diff(self, ctx, ref=None):
338 342
339 343 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
340 344 prev = ctx.p1().node()
341 345 if ref:
342 346 ref = ref.node()
343 347 else:
344 348 ref = ctx.node()
345 349 chunks = patch.diff(self.repo, prev, ref,
346 350 opts=patch.diffallopts(self.ui))
347 351 difflines = ''.join(chunks).splitlines()
348 352
349 353 if self.ui.configbool('notify', 'diffstat', True):
350 354 s = patch.diffstat(difflines)
351 355 # s may be nil, don't include the header if it is
352 356 if s:
353 357 self.ui.write('\ndiffstat:\n\n%s' % s)
354 358
355 359 if maxdiff == 0:
356 360 return
357 361 elif maxdiff > 0 and len(difflines) > maxdiff:
358 362 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
359 363 self.ui.write(msg % (len(difflines), maxdiff))
360 364 difflines = difflines[:maxdiff]
361 365 elif difflines:
362 366 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
363 367
364 368 self.ui.write("\n".join(difflines))
365 369
366 370 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
367 371 '''send email notifications to interested subscribers.
368 372
369 373 if used as changegroup hook, send one email for all changesets in
370 374 changegroup. else send one email per changeset.'''
371 375
372 376 n = notifier(ui, repo, hooktype)
373 377 ctx = repo[node]
374 378
375 379 if not n.subs:
376 380 ui.debug('notify: no subscribers to repository %s\n' % n.root)
377 381 return
378 382 if n.skipsource(source):
379 383 ui.debug('notify: changes have source "%s" - skipping\n' % source)
380 384 return
381 385
382 386 ui.pushbuffer()
383 387 data = ''
384 388 count = 0
385 389 author = ''
386 390 if hooktype == 'changegroup' or hooktype == 'outgoing':
387 391 start, end = ctx.rev(), len(repo)
388 392 for rev in xrange(start, end):
389 393 if n.node(repo[rev]):
390 394 count += 1
391 395 if not author:
392 396 author = repo[rev].user()
393 397 else:
394 398 data += ui.popbuffer()
395 399 ui.note(_('notify: suppressing notification for merge %d:%s\n')
396 400 % (rev, repo[rev].hex()[:12]))
397 401 ui.pushbuffer()
398 402 if count:
399 403 n.diff(ctx, repo['tip'])
400 404 else:
401 405 if not n.node(ctx):
402 406 ui.popbuffer()
403 407 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
404 408 (ctx.rev(), ctx.hex()[:12]))
405 409 return
406 410 count += 1
407 411 n.diff(ctx)
408 412
409 413 data += ui.popbuffer()
410 414 fromauthor = ui.config('notify', 'fromauthor')
411 415 if author and fromauthor:
412 416 data = '\n'.join(['From: %s' % author, data])
413 417
414 418 if count:
415 419 n.send(ctx, count, data)
@@ -1,175 +1,179 b''
1 1 # pager.py - display output using a pager
2 2 #
3 3 # Copyright 2008 David Soria Parra <dsp@php.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 # To load the extension, add it to your configuration file:
9 9 #
10 10 # [extension]
11 11 # pager =
12 12 #
13 13 # Run "hg help pager" to get info on configuration.
14 14
15 15 '''browse command output with an external pager
16 16
17 17 To set the pager that should be used, set the application variable::
18 18
19 19 [pager]
20 20 pager = less -FRX
21 21
22 22 If no pager is set, the pager extensions uses the environment variable
23 23 $PAGER. If neither pager.pager, nor $PAGER is set, no pager is used.
24 24
25 25 You can disable the pager for certain commands by adding them to the
26 26 pager.ignore list::
27 27
28 28 [pager]
29 29 ignore = version, help, update
30 30
31 31 You can also enable the pager only for certain commands using
32 32 pager.attend. Below is the default list of commands to be paged::
33 33
34 34 [pager]
35 35 attend = annotate, cat, diff, export, glog, log, qdiff
36 36
37 37 Setting pager.attend to an empty value will cause all commands to be
38 38 paged.
39 39
40 40 If pager.attend is present, pager.ignore will be ignored.
41 41
42 42 Lastly, you can enable and disable paging for individual commands with
43 43 the attend-<command> option. This setting takes precedence over
44 44 existing attend and ignore options and defaults::
45 45
46 46 [pager]
47 47 attend-cat = false
48 48
49 49 To ignore global commands like :hg:`version` or :hg:`help`, you have
50 50 to specify them in your user configuration file.
51 51
52 52 The --pager=... option can also be used to control when the pager is
53 53 used. Use a boolean value like yes, no, on, off, or use auto for
54 54 normal behavior.
55 55
56 56 '''
57 57
58 58 import atexit, sys, os, signal, subprocess, errno, shlex
59 59 from mercurial import commands, dispatch, util, extensions, cmdutil
60 60 from mercurial.i18n import _
61 61
62 # Note for extension authors: ONLY specify testedwith = 'internal' for
63 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
64 # be specifying the version(s) of Mercurial they are tested with, or
65 # leave the attribute unspecified.
62 66 testedwith = 'internal'
63 67
64 68 def _pagerfork(ui, p):
65 69 if not util.safehasattr(os, 'fork'):
66 70 sys.stdout = util.popen(p, 'wb')
67 71 if ui._isatty(sys.stderr):
68 72 sys.stderr = sys.stdout
69 73 return
70 74 fdin, fdout = os.pipe()
71 75 pid = os.fork()
72 76 if pid == 0:
73 77 os.close(fdin)
74 78 os.dup2(fdout, sys.stdout.fileno())
75 79 if ui._isatty(sys.stderr):
76 80 os.dup2(fdout, sys.stderr.fileno())
77 81 os.close(fdout)
78 82 return
79 83 os.dup2(fdin, sys.stdin.fileno())
80 84 os.close(fdin)
81 85 os.close(fdout)
82 86 try:
83 87 os.execvp('/bin/sh', ['/bin/sh', '-c', p])
84 88 except OSError, e:
85 89 if e.errno == errno.ENOENT:
86 90 # no /bin/sh, try executing the pager directly
87 91 args = shlex.split(p)
88 92 os.execvp(args[0], args)
89 93 else:
90 94 raise
91 95
92 96 def _pagersubprocess(ui, p):
93 97 pager = subprocess.Popen(p, shell=True, bufsize=-1,
94 98 close_fds=util.closefds, stdin=subprocess.PIPE,
95 99 stdout=sys.stdout, stderr=sys.stderr)
96 100
97 101 stdout = os.dup(sys.stdout.fileno())
98 102 stderr = os.dup(sys.stderr.fileno())
99 103 os.dup2(pager.stdin.fileno(), sys.stdout.fileno())
100 104 if ui._isatty(sys.stderr):
101 105 os.dup2(pager.stdin.fileno(), sys.stderr.fileno())
102 106
103 107 @atexit.register
104 108 def killpager():
105 109 if util.safehasattr(signal, "SIGINT"):
106 110 signal.signal(signal.SIGINT, signal.SIG_IGN)
107 111 pager.stdin.close()
108 112 os.dup2(stdout, sys.stdout.fileno())
109 113 os.dup2(stderr, sys.stderr.fileno())
110 114 pager.wait()
111 115
112 116 def _runpager(ui, p):
113 117 # The subprocess module shipped with Python <= 2.4 is buggy (issue3533).
114 118 # The compat version is buggy on Windows (issue3225), but has been shipping
115 119 # with hg for a long time. Preserve existing functionality.
116 120 if sys.version_info >= (2, 5):
117 121 _pagersubprocess(ui, p)
118 122 else:
119 123 _pagerfork(ui, p)
120 124
121 125 def uisetup(ui):
122 126 if '--debugger' in sys.argv or not ui.formatted():
123 127 return
124 128
125 129 def pagecmd(orig, ui, options, cmd, cmdfunc):
126 130 p = ui.config("pager", "pager", os.environ.get("PAGER"))
127 131 usepager = False
128 132 always = util.parsebool(options['pager'])
129 133 auto = options['pager'] == 'auto'
130 134
131 135 if not p:
132 136 pass
133 137 elif always:
134 138 usepager = True
135 139 elif not auto:
136 140 usepager = False
137 141 else:
138 142 attend = ui.configlist('pager', 'attend', attended)
139 143 ignore = ui.configlist('pager', 'ignore')
140 144 cmds, _ = cmdutil.findcmd(cmd, commands.table)
141 145
142 146 for cmd in cmds:
143 147 var = 'attend-%s' % cmd
144 148 if ui.config('pager', var):
145 149 usepager = ui.configbool('pager', var)
146 150 break
147 151 if (cmd in attend or
148 152 (cmd not in ignore and not attend)):
149 153 usepager = True
150 154 break
151 155
152 156 setattr(ui, 'pageractive', usepager)
153 157
154 158 if usepager:
155 159 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
156 160 ui.setconfig('ui', 'interactive', False, 'pager')
157 161 if util.safehasattr(signal, "SIGPIPE"):
158 162 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
159 163 _runpager(ui, p)
160 164 return orig(ui, options, cmd, cmdfunc)
161 165
162 166 # Wrap dispatch._runcommand after color is loaded so color can see
163 167 # ui.pageractive. Otherwise, if we loaded first, color's wrapped
164 168 # dispatch._runcommand would run without having access to ui.pageractive.
165 169 def afterloaded(loaded):
166 170 extensions.wrapfunction(dispatch, '_runcommand', pagecmd)
167 171 extensions.afterloaded('color', afterloaded)
168 172
169 173 def extsetup(ui):
170 174 commands.globalopts.append(
171 175 ('', 'pager', 'auto',
172 176 _("when to paginate (boolean, always, auto, or never)"),
173 177 _('TYPE')))
174 178
175 179 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
@@ -1,656 +1,660 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 You can control the default inclusion of an introduction message with the
48 48 ``patchbomb.intro`` configuration option. The configuration is always
49 49 overwritten by command line flags like --intro and --desc::
50 50
51 51 [patchbomb]
52 52 intro=auto # include introduction message if more than 1 patch (default)
53 53 intro=never # never include an introduction message
54 54 intro=always # always include an introduction message
55 55
56 56 You can set patchbomb to always ask for confirmation by setting
57 57 ``patchbomb.confirm`` to true.
58 58 '''
59 59
60 60 import os, errno, socket, tempfile, cStringIO
61 61 import email
62 62 # On python2.4 you have to import these by name or they fail to
63 63 # load. This was not a problem on Python 2.7.
64 64 import email.Generator
65 65 import email.MIMEMultipart
66 66
67 67 from mercurial import cmdutil, commands, hg, mail, patch, util
68 68 from mercurial import scmutil
69 69 from mercurial.i18n import _
70 70 from mercurial.node import bin
71 71
72 72 cmdtable = {}
73 73 command = cmdutil.command(cmdtable)
74 # Note for extension authors: ONLY specify testedwith = 'internal' for
75 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
76 # be specifying the version(s) of Mercurial they are tested with, or
77 # leave the attribute unspecified.
74 78 testedwith = 'internal'
75 79
76 80 def prompt(ui, prompt, default=None, rest=':'):
77 81 if default:
78 82 prompt += ' [%s]' % default
79 83 return ui.prompt(prompt + rest, default)
80 84
81 85 def introwanted(ui, opts, number):
82 86 '''is an introductory message apparently wanted?'''
83 87 introconfig = ui.config('patchbomb', 'intro', 'auto')
84 88 if opts.get('intro') or opts.get('desc'):
85 89 intro = True
86 90 elif introconfig == 'always':
87 91 intro = True
88 92 elif introconfig == 'never':
89 93 intro = False
90 94 elif introconfig == 'auto':
91 95 intro = 1 < number
92 96 else:
93 97 ui.write_err(_('warning: invalid patchbomb.intro value "%s"\n')
94 98 % introconfig)
95 99 ui.write_err(_('(should be one of always, never, auto)\n'))
96 100 intro = 1 < number
97 101 return intro
98 102
99 103 def makepatch(ui, repo, patchlines, opts, _charsets, idx, total, numbered,
100 104 patchname=None):
101 105
102 106 desc = []
103 107 node = None
104 108 body = ''
105 109
106 110 for line in patchlines:
107 111 if line.startswith('#'):
108 112 if line.startswith('# Node ID'):
109 113 node = line.split()[-1]
110 114 continue
111 115 if line.startswith('diff -r') or line.startswith('diff --git'):
112 116 break
113 117 desc.append(line)
114 118
115 119 if not patchname and not node:
116 120 raise ValueError
117 121
118 122 if opts.get('attach') and not opts.get('body'):
119 123 body = ('\n'.join(desc[1:]).strip() or
120 124 'Patch subject is complete summary.')
121 125 body += '\n\n\n'
122 126
123 127 if opts.get('plain'):
124 128 while patchlines and patchlines[0].startswith('# '):
125 129 patchlines.pop(0)
126 130 if patchlines:
127 131 patchlines.pop(0)
128 132 while patchlines and not patchlines[0].strip():
129 133 patchlines.pop(0)
130 134
131 135 ds = patch.diffstat(patchlines, git=opts.get('git'))
132 136 if opts.get('diffstat'):
133 137 body += ds + '\n\n'
134 138
135 139 addattachment = opts.get('attach') or opts.get('inline')
136 140 if not addattachment or opts.get('body'):
137 141 body += '\n'.join(patchlines)
138 142
139 143 if addattachment:
140 144 msg = email.MIMEMultipart.MIMEMultipart()
141 145 if body:
142 146 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
143 147 p = mail.mimetextpatch('\n'.join(patchlines), 'x-patch',
144 148 opts.get('test'))
145 149 binnode = bin(node)
146 150 # if node is mq patch, it will have the patch file's name as a tag
147 151 if not patchname:
148 152 patchtags = [t for t in repo.nodetags(binnode)
149 153 if t.endswith('.patch') or t.endswith('.diff')]
150 154 if patchtags:
151 155 patchname = patchtags[0]
152 156 elif total > 1:
153 157 patchname = cmdutil.makefilename(repo, '%b-%n.patch',
154 158 binnode, seqno=idx,
155 159 total=total)
156 160 else:
157 161 patchname = cmdutil.makefilename(repo, '%b.patch', binnode)
158 162 disposition = 'inline'
159 163 if opts.get('attach'):
160 164 disposition = 'attachment'
161 165 p['Content-Disposition'] = disposition + '; filename=' + patchname
162 166 msg.attach(p)
163 167 else:
164 168 msg = mail.mimetextpatch(body, display=opts.get('test'))
165 169
166 170 flag = ' '.join(opts.get('flag'))
167 171 if flag:
168 172 flag = ' ' + flag
169 173
170 174 subj = desc[0].strip().rstrip('. ')
171 175 if not numbered:
172 176 subj = '[PATCH%s] %s' % (flag, opts.get('subject') or subj)
173 177 else:
174 178 tlen = len(str(total))
175 179 subj = '[PATCH %0*d of %d%s] %s' % (tlen, idx, total, flag, subj)
176 180 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
177 181 msg['X-Mercurial-Node'] = node
178 182 msg['X-Mercurial-Series-Index'] = '%i' % idx
179 183 msg['X-Mercurial-Series-Total'] = '%i' % total
180 184 return msg, subj, ds
181 185
182 186 def _getpatches(repo, revs, **opts):
183 187 """return a list of patches for a list of revisions
184 188
185 189 Each patch in the list is itself a list of lines.
186 190 """
187 191 ui = repo.ui
188 192 prev = repo['.'].rev()
189 193 for r in revs:
190 194 if r == prev and (repo[None].files() or repo[None].deleted()):
191 195 ui.warn(_('warning: working directory has '
192 196 'uncommitted changes\n'))
193 197 output = cStringIO.StringIO()
194 198 cmdutil.export(repo, [r], fp=output,
195 199 opts=patch.difffeatureopts(ui, opts, git=True))
196 200 yield output.getvalue().split('\n')
197 201 def _getbundle(repo, dest, **opts):
198 202 """return a bundle containing changesets missing in "dest"
199 203
200 204 The `opts` keyword-arguments are the same as the one accepted by the
201 205 `bundle` command.
202 206
203 207 The bundle is a returned as a single in-memory binary blob.
204 208 """
205 209 ui = repo.ui
206 210 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
207 211 tmpfn = os.path.join(tmpdir, 'bundle')
208 212 try:
209 213 commands.bundle(ui, repo, tmpfn, dest, **opts)
210 214 fp = open(tmpfn, 'rb')
211 215 data = fp.read()
212 216 fp.close()
213 217 return data
214 218 finally:
215 219 try:
216 220 os.unlink(tmpfn)
217 221 except OSError:
218 222 pass
219 223 os.rmdir(tmpdir)
220 224
221 225 def _getdescription(repo, defaultbody, sender, **opts):
222 226 """obtain the body of the introduction message and return it
223 227
224 228 This is also used for the body of email with an attached bundle.
225 229
226 230 The body can be obtained either from the command line option or entered by
227 231 the user through the editor.
228 232 """
229 233 ui = repo.ui
230 234 if opts.get('desc'):
231 235 body = open(opts.get('desc')).read()
232 236 else:
233 237 ui.write(_('\nWrite the introductory message for the '
234 238 'patch series.\n\n'))
235 239 body = ui.edit(defaultbody, sender)
236 240 # Save series description in case sendmail fails
237 241 msgfile = repo.vfs('last-email.txt', 'wb')
238 242 msgfile.write(body)
239 243 msgfile.close()
240 244 return body
241 245
242 246 def _getbundlemsgs(repo, sender, bundle, **opts):
243 247 """Get the full email for sending a given bundle
244 248
245 249 This function returns a list of "email" tuples (subject, content, None).
246 250 The list is always one message long in that case.
247 251 """
248 252 ui = repo.ui
249 253 _charsets = mail._charsets(ui)
250 254 subj = (opts.get('subject')
251 255 or prompt(ui, 'Subject:', 'A bundle for your repository'))
252 256
253 257 body = _getdescription(repo, '', sender, **opts)
254 258 msg = email.MIMEMultipart.MIMEMultipart()
255 259 if body:
256 260 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
257 261 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
258 262 datapart.set_payload(bundle)
259 263 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
260 264 datapart.add_header('Content-Disposition', 'attachment',
261 265 filename=bundlename)
262 266 email.Encoders.encode_base64(datapart)
263 267 msg.attach(datapart)
264 268 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
265 269 return [(msg, subj, None)]
266 270
267 271 def _makeintro(repo, sender, patches, **opts):
268 272 """make an introduction email, asking the user for content if needed
269 273
270 274 email is returned as (subject, body, cumulative-diffstat)"""
271 275 ui = repo.ui
272 276 _charsets = mail._charsets(ui)
273 277 tlen = len(str(len(patches)))
274 278
275 279 flag = opts.get('flag') or ''
276 280 if flag:
277 281 flag = ' ' + ' '.join(flag)
278 282 prefix = '[PATCH %0*d of %d%s]' % (tlen, 0, len(patches), flag)
279 283
280 284 subj = (opts.get('subject') or
281 285 prompt(ui, '(optional) Subject: ', rest=prefix, default=''))
282 286 if not subj:
283 287 return None # skip intro if the user doesn't bother
284 288
285 289 subj = prefix + ' ' + subj
286 290
287 291 body = ''
288 292 if opts.get('diffstat'):
289 293 # generate a cumulative diffstat of the whole patch series
290 294 diffstat = patch.diffstat(sum(patches, []))
291 295 body = '\n' + diffstat
292 296 else:
293 297 diffstat = None
294 298
295 299 body = _getdescription(repo, body, sender, **opts)
296 300 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
297 301 msg['Subject'] = mail.headencode(ui, subj, _charsets,
298 302 opts.get('test'))
299 303 return (msg, subj, diffstat)
300 304
301 305 def _getpatchmsgs(repo, sender, patches, patchnames=None, **opts):
302 306 """return a list of emails from a list of patches
303 307
304 308 This involves introduction message creation if necessary.
305 309
306 310 This function returns a list of "email" tuples (subject, content, None).
307 311 """
308 312 ui = repo.ui
309 313 _charsets = mail._charsets(ui)
310 314 msgs = []
311 315
312 316 ui.write(_('this patch series consists of %d patches.\n\n')
313 317 % len(patches))
314 318
315 319 # build the intro message, or skip it if the user declines
316 320 if introwanted(ui, opts, len(patches)):
317 321 msg = _makeintro(repo, sender, patches, **opts)
318 322 if msg:
319 323 msgs.append(msg)
320 324
321 325 # are we going to send more than one message?
322 326 numbered = len(msgs) + len(patches) > 1
323 327
324 328 # now generate the actual patch messages
325 329 name = None
326 330 for i, p in enumerate(patches):
327 331 if patchnames:
328 332 name = patchnames[i]
329 333 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
330 334 len(patches), numbered, name)
331 335 msgs.append(msg)
332 336
333 337 return msgs
334 338
335 339 def _getoutgoing(repo, dest, revs):
336 340 '''Return the revisions present locally but not in dest'''
337 341 ui = repo.ui
338 342 url = ui.expandpath(dest or 'default-push', dest or 'default')
339 343 url = hg.parseurl(url)[0]
340 344 ui.status(_('comparing with %s\n') % util.hidepassword(url))
341 345
342 346 revs = [r for r in revs if r >= 0]
343 347 if not revs:
344 348 revs = [len(repo) - 1]
345 349 revs = repo.revs('outgoing(%s) and ::%ld', dest or '', revs)
346 350 if not revs:
347 351 ui.status(_("no changes found\n"))
348 352 return revs
349 353
350 354 emailopts = [
351 355 ('', 'body', None, _('send patches as inline message text (default)')),
352 356 ('a', 'attach', None, _('send patches as attachments')),
353 357 ('i', 'inline', None, _('send patches as inline attachments')),
354 358 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
355 359 ('c', 'cc', [], _('email addresses of copy recipients')),
356 360 ('', 'confirm', None, _('ask for confirmation before sending')),
357 361 ('d', 'diffstat', None, _('add diffstat output to messages')),
358 362 ('', 'date', '', _('use the given date as the sending date')),
359 363 ('', 'desc', '', _('use the given file as the series description')),
360 364 ('f', 'from', '', _('email address of sender')),
361 365 ('n', 'test', None, _('print messages that would be sent')),
362 366 ('m', 'mbox', '', _('write messages to mbox file instead of sending them')),
363 367 ('', 'reply-to', [], _('email addresses replies should be sent to')),
364 368 ('s', 'subject', '', _('subject of first message (intro or single patch)')),
365 369 ('', 'in-reply-to', '', _('message identifier to reply to')),
366 370 ('', 'flag', [], _('flags to add in subject prefixes')),
367 371 ('t', 'to', [], _('email addresses of recipients'))]
368 372
369 373 @command('email',
370 374 [('g', 'git', None, _('use git extended diff format')),
371 375 ('', 'plain', None, _('omit hg patch header')),
372 376 ('o', 'outgoing', None,
373 377 _('send changes not found in the target repository')),
374 378 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
375 379 ('', 'bundlename', 'bundle',
376 380 _('name of the bundle attachment file'), _('NAME')),
377 381 ('r', 'rev', [], _('a revision to send'), _('REV')),
378 382 ('', 'force', None, _('run even when remote repository is unrelated '
379 383 '(with -b/--bundle)')),
380 384 ('', 'base', [], _('a base changeset to specify instead of a destination '
381 385 '(with -b/--bundle)'), _('REV')),
382 386 ('', 'intro', None, _('send an introduction email for a single patch')),
383 387 ] + emailopts + commands.remoteopts,
384 388 _('hg email [OPTION]... [DEST]...'))
385 389 def patchbomb(ui, repo, *revs, **opts):
386 390 '''send changesets by email
387 391
388 392 By default, diffs are sent in the format generated by
389 393 :hg:`export`, one per message. The series starts with a "[PATCH 0
390 394 of N]" introduction, which describes the series as a whole.
391 395
392 396 Each patch email has a Subject line of "[PATCH M of N] ...", using
393 397 the first line of the changeset description as the subject text.
394 398 The message contains two or three parts. First, the changeset
395 399 description.
396 400
397 401 With the -d/--diffstat option, if the diffstat program is
398 402 installed, the result of running diffstat on the patch is inserted.
399 403
400 404 Finally, the patch itself, as generated by :hg:`export`.
401 405
402 406 With the -d/--diffstat or --confirm options, you will be presented
403 407 with a final summary of all messages and asked for confirmation before
404 408 the messages are sent.
405 409
406 410 By default the patch is included as text in the email body for
407 411 easy reviewing. Using the -a/--attach option will instead create
408 412 an attachment for the patch. With -i/--inline an inline attachment
409 413 will be created. You can include a patch both as text in the email
410 414 body and as a regular or an inline attachment by combining the
411 415 -a/--attach or -i/--inline with the --body option.
412 416
413 417 With -o/--outgoing, emails will be generated for patches not found
414 418 in the destination repository (or only those which are ancestors
415 419 of the specified revisions if any are provided)
416 420
417 421 With -b/--bundle, changesets are selected as for --outgoing, but a
418 422 single email containing a binary Mercurial bundle as an attachment
419 423 will be sent.
420 424
421 425 With -m/--mbox, instead of previewing each patchbomb message in a
422 426 pager or sending the messages directly, it will create a UNIX
423 427 mailbox file with the patch emails. This mailbox file can be
424 428 previewed with any mail user agent which supports UNIX mbox
425 429 files.
426 430
427 431 With -n/--test, all steps will run, but mail will not be sent.
428 432 You will be prompted for an email recipient address, a subject and
429 433 an introductory message describing the patches of your patchbomb.
430 434 Then when all is done, patchbomb messages are displayed. If the
431 435 PAGER environment variable is set, your pager will be fired up once
432 436 for each patchbomb message, so you can verify everything is alright.
433 437
434 438 In case email sending fails, you will find a backup of your series
435 439 introductory message in ``.hg/last-email.txt``.
436 440
437 441 The default behavior of this command can be customized through
438 442 configuration. (See :hg:`help patchbomb` for details)
439 443
440 444 Examples::
441 445
442 446 hg email -r 3000 # send patch 3000 only
443 447 hg email -r 3000 -r 3001 # send patches 3000 and 3001
444 448 hg email -r 3000:3005 # send patches 3000 through 3005
445 449 hg email 3000 # send patch 3000 (deprecated)
446 450
447 451 hg email -o # send all patches not in default
448 452 hg email -o DEST # send all patches not in DEST
449 453 hg email -o -r 3000 # send all ancestors of 3000 not in default
450 454 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
451 455
452 456 hg email -b # send bundle of all patches not in default
453 457 hg email -b DEST # send bundle of all patches not in DEST
454 458 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
455 459 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
456 460
457 461 hg email -o -m mbox && # generate an mbox file...
458 462 mutt -R -f mbox # ... and view it with mutt
459 463 hg email -o -m mbox && # generate an mbox file ...
460 464 formail -s sendmail \\ # ... and use formail to send from the mbox
461 465 -bm -t < mbox # ... using sendmail
462 466
463 467 Before using this command, you will need to enable email in your
464 468 hgrc. See the [email] section in hgrc(5) for details.
465 469 '''
466 470
467 471 _charsets = mail._charsets(ui)
468 472
469 473 bundle = opts.get('bundle')
470 474 date = opts.get('date')
471 475 mbox = opts.get('mbox')
472 476 outgoing = opts.get('outgoing')
473 477 rev = opts.get('rev')
474 478 # internal option used by pbranches
475 479 patches = opts.get('patches')
476 480
477 481 if not (opts.get('test') or mbox):
478 482 # really sending
479 483 mail.validateconfig(ui)
480 484
481 485 if not (revs or rev or outgoing or bundle or patches):
482 486 raise util.Abort(_('specify at least one changeset with -r or -o'))
483 487
484 488 if outgoing and bundle:
485 489 raise util.Abort(_("--outgoing mode always on with --bundle;"
486 490 " do not re-specify --outgoing"))
487 491
488 492 if outgoing or bundle:
489 493 if len(revs) > 1:
490 494 raise util.Abort(_("too many destinations"))
491 495 if revs:
492 496 dest = revs[0]
493 497 else:
494 498 dest = None
495 499 revs = []
496 500
497 501 if rev:
498 502 if revs:
499 503 raise util.Abort(_('use only one form to specify the revision'))
500 504 revs = rev
501 505
502 506 revs = scmutil.revrange(repo, revs)
503 507 if outgoing:
504 508 revs = _getoutgoing(repo, dest, revs)
505 509 if bundle:
506 510 opts['revs'] = [str(r) for r in revs]
507 511
508 512 # start
509 513 if date:
510 514 start_time = util.parsedate(date)
511 515 else:
512 516 start_time = util.makedate()
513 517
514 518 def genmsgid(id):
515 519 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
516 520
517 521 sender = (opts.get('from') or ui.config('email', 'from') or
518 522 ui.config('patchbomb', 'from') or
519 523 prompt(ui, 'From', ui.username()))
520 524
521 525 if patches:
522 526 msgs = _getpatchmsgs(repo, sender, patches, opts.get('patchnames'),
523 527 **opts)
524 528 elif bundle:
525 529 bundledata = _getbundle(repo, dest, **opts)
526 530 bundleopts = opts.copy()
527 531 bundleopts.pop('bundle', None) # already processed
528 532 msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts)
529 533 else:
530 534 _patches = list(_getpatches(repo, revs, **opts))
531 535 msgs = _getpatchmsgs(repo, sender, _patches, **opts)
532 536
533 537 showaddrs = []
534 538
535 539 def getaddrs(header, ask=False, default=None):
536 540 configkey = header.lower()
537 541 opt = header.replace('-', '_').lower()
538 542 addrs = opts.get(opt)
539 543 if addrs:
540 544 showaddrs.append('%s: %s' % (header, ', '.join(addrs)))
541 545 return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
542 546
543 547 # not on the command line: fallback to config and then maybe ask
544 548 addr = (ui.config('email', configkey) or
545 549 ui.config('patchbomb', configkey) or
546 550 '')
547 551 if not addr and ask:
548 552 addr = prompt(ui, header, default=default)
549 553 if addr:
550 554 showaddrs.append('%s: %s' % (header, addr))
551 555 return mail.addrlistencode(ui, [addr], _charsets, opts.get('test'))
552 556 else:
553 557 return default
554 558
555 559 to = getaddrs('To', ask=True)
556 560 if not to:
557 561 # we can get here in non-interactive mode
558 562 raise util.Abort(_('no recipient addresses provided'))
559 563 cc = getaddrs('Cc', ask=True, default='') or []
560 564 bcc = getaddrs('Bcc') or []
561 565 replyto = getaddrs('Reply-To')
562 566
563 567 confirm = ui.configbool('patchbomb', 'confirm')
564 568 confirm |= bool(opts.get('diffstat') or opts.get('confirm'))
565 569
566 570 if confirm:
567 571 ui.write(_('\nFinal summary:\n\n'), label='patchbomb.finalsummary')
568 572 ui.write(('From: %s\n' % sender), label='patchbomb.from')
569 573 for addr in showaddrs:
570 574 ui.write('%s\n' % addr, label='patchbomb.to')
571 575 for m, subj, ds in msgs:
572 576 ui.write(('Subject: %s\n' % subj), label='patchbomb.subject')
573 577 if ds:
574 578 ui.write(ds, label='patchbomb.diffstats')
575 579 ui.write('\n')
576 580 if ui.promptchoice(_('are you sure you want to send (yn)?'
577 581 '$$ &Yes $$ &No')):
578 582 raise util.Abort(_('patchbomb canceled'))
579 583
580 584 ui.write('\n')
581 585
582 586 parent = opts.get('in_reply_to') or None
583 587 # angle brackets may be omitted, they're not semantically part of the msg-id
584 588 if parent is not None:
585 589 if not parent.startswith('<'):
586 590 parent = '<' + parent
587 591 if not parent.endswith('>'):
588 592 parent += '>'
589 593
590 594 sender_addr = email.Utils.parseaddr(sender)[1]
591 595 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
592 596 sendmail = None
593 597 firstpatch = None
594 598 for i, (m, subj, ds) in enumerate(msgs):
595 599 try:
596 600 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
597 601 if not firstpatch:
598 602 firstpatch = m['Message-Id']
599 603 m['X-Mercurial-Series-Id'] = firstpatch
600 604 except TypeError:
601 605 m['Message-Id'] = genmsgid('patchbomb')
602 606 if parent:
603 607 m['In-Reply-To'] = parent
604 608 m['References'] = parent
605 609 if not parent or 'X-Mercurial-Node' not in m:
606 610 parent = m['Message-Id']
607 611
608 612 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
609 613 m['Date'] = email.Utils.formatdate(start_time[0], localtime=True)
610 614
611 615 start_time = (start_time[0] + 1, start_time[1])
612 616 m['From'] = sender
613 617 m['To'] = ', '.join(to)
614 618 if cc:
615 619 m['Cc'] = ', '.join(cc)
616 620 if bcc:
617 621 m['Bcc'] = ', '.join(bcc)
618 622 if replyto:
619 623 m['Reply-To'] = ', '.join(replyto)
620 624 if opts.get('test'):
621 625 ui.status(_('displaying '), subj, ' ...\n')
622 626 ui.flush()
623 627 if 'PAGER' in os.environ and not ui.plain():
624 628 fp = util.popen(os.environ['PAGER'], 'w')
625 629 else:
626 630 fp = ui
627 631 generator = email.Generator.Generator(fp, mangle_from_=False)
628 632 try:
629 633 generator.flatten(m, 0)
630 634 fp.write('\n')
631 635 except IOError, inst:
632 636 if inst.errno != errno.EPIPE:
633 637 raise
634 638 if fp is not ui:
635 639 fp.close()
636 640 else:
637 641 if not sendmail:
638 642 verifycert = ui.config('smtp', 'verifycert')
639 643 if opts.get('insecure'):
640 644 ui.setconfig('smtp', 'verifycert', 'loose', 'patchbomb')
641 645 try:
642 646 sendmail = mail.connect(ui, mbox=mbox)
643 647 finally:
644 648 ui.setconfig('smtp', 'verifycert', verifycert, 'patchbomb')
645 649 ui.status(_('sending '), subj, ' ...\n')
646 650 ui.progress(_('sending'), i, item=subj, total=len(msgs))
647 651 if not mbox:
648 652 # Exim does not remove the Bcc field
649 653 del m['Bcc']
650 654 fp = cStringIO.StringIO()
651 655 generator = email.Generator.Generator(fp, mangle_from_=False)
652 656 generator.flatten(m, 0)
653 657 sendmail(sender_addr, to + bcc + cc, fp.getvalue())
654 658
655 659 ui.progress(_('writing'), None)
656 660 ui.progress(_('sending'), None)
@@ -1,320 +1,324 b''
1 1 # progress.py show progress bars for some actions
2 2 #
3 3 # Copyright (C) 2010 Augie Fackler <durin42@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 """show progress bars for some actions
9 9
10 10 This extension uses the progress information logged by hg commands
11 11 to draw progress bars that are as informative as possible. Some progress
12 12 bars only offer indeterminate information, while others have a definite
13 13 end point.
14 14
15 15 The following settings are available::
16 16
17 17 [progress]
18 18 delay = 3 # number of seconds (float) before showing the progress bar
19 19 changedelay = 1 # changedelay: minimum delay before showing a new topic.
20 20 # If set to less than 3 * refresh, that value will
21 21 # be used instead.
22 22 refresh = 0.1 # time in seconds between refreshes of the progress bar
23 23 format = topic bar number estimate # format of the progress bar
24 24 width = <none> # if set, the maximum width of the progress information
25 25 # (that is, min(width, term width) will be used)
26 26 clear-complete = True # clear the progress bar after it's done
27 27 disable = False # if true, don't show a progress bar
28 28 assume-tty = False # if true, ALWAYS show a progress bar, unless
29 29 # disable is given
30 30
31 31 Valid entries for the format field are topic, bar, number, unit,
32 32 estimate, speed, and item. item defaults to the last 20 characters of
33 33 the item, but this can be changed by adding either ``-<num>`` which
34 34 would take the last num characters, or ``+<num>`` for the first num
35 35 characters.
36 36 """
37 37
38 38 import sys
39 39 import time
40 40 import threading
41 41
42 42 from mercurial.i18n import _
43 # Note for extension authors: ONLY specify testedwith = 'internal' for
44 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
45 # be specifying the version(s) of Mercurial they are tested with, or
46 # leave the attribute unspecified.
43 47 testedwith = 'internal'
44 48
45 49 from mercurial import encoding
46 50
47 51 def spacejoin(*args):
48 52 return ' '.join(s for s in args if s)
49 53
50 54 def shouldprint(ui):
51 55 return not ui.plain() and (ui._isatty(sys.stderr) or
52 56 ui.configbool('progress', 'assume-tty'))
53 57
54 58 def fmtremaining(seconds):
55 59 if seconds < 60:
56 60 # i18n: format XX seconds as "XXs"
57 61 return _("%02ds") % (seconds)
58 62 minutes = seconds // 60
59 63 if minutes < 60:
60 64 seconds -= minutes * 60
61 65 # i18n: format X minutes and YY seconds as "XmYYs"
62 66 return _("%dm%02ds") % (minutes, seconds)
63 67 # we're going to ignore seconds in this case
64 68 minutes += 1
65 69 hours = minutes // 60
66 70 minutes -= hours * 60
67 71 if hours < 30:
68 72 # i18n: format X hours and YY minutes as "XhYYm"
69 73 return _("%dh%02dm") % (hours, minutes)
70 74 # we're going to ignore minutes in this case
71 75 hours += 1
72 76 days = hours // 24
73 77 hours -= days * 24
74 78 if days < 15:
75 79 # i18n: format X days and YY hours as "XdYYh"
76 80 return _("%dd%02dh") % (days, hours)
77 81 # we're going to ignore hours in this case
78 82 days += 1
79 83 weeks = days // 7
80 84 days -= weeks * 7
81 85 if weeks < 55:
82 86 # i18n: format X weeks and YY days as "XwYYd"
83 87 return _("%dw%02dd") % (weeks, days)
84 88 # we're going to ignore days and treat a year as 52 weeks
85 89 weeks += 1
86 90 years = weeks // 52
87 91 weeks -= years * 52
88 92 # i18n: format X years and YY weeks as "XyYYw"
89 93 return _("%dy%02dw") % (years, weeks)
90 94
91 95 class progbar(object):
92 96 def __init__(self, ui):
93 97 self.ui = ui
94 98 self._refreshlock = threading.Lock()
95 99 self.resetstate()
96 100
97 101 def resetstate(self):
98 102 self.topics = []
99 103 self.topicstates = {}
100 104 self.starttimes = {}
101 105 self.startvals = {}
102 106 self.printed = False
103 107 self.lastprint = time.time() + float(self.ui.config(
104 108 'progress', 'delay', default=3))
105 109 self.curtopic = None
106 110 self.lasttopic = None
107 111 self.indetcount = 0
108 112 self.refresh = float(self.ui.config(
109 113 'progress', 'refresh', default=0.1))
110 114 self.changedelay = max(3 * self.refresh,
111 115 float(self.ui.config(
112 116 'progress', 'changedelay', default=1)))
113 117 self.order = self.ui.configlist(
114 118 'progress', 'format',
115 119 default=['topic', 'bar', 'number', 'estimate'])
116 120
117 121 def show(self, now, topic, pos, item, unit, total):
118 122 if not shouldprint(self.ui):
119 123 return
120 124 termwidth = self.width()
121 125 self.printed = True
122 126 head = ''
123 127 needprogress = False
124 128 tail = ''
125 129 for indicator in self.order:
126 130 add = ''
127 131 if indicator == 'topic':
128 132 add = topic
129 133 elif indicator == 'number':
130 134 if total:
131 135 add = ('% ' + str(len(str(total))) +
132 136 's/%s') % (pos, total)
133 137 else:
134 138 add = str(pos)
135 139 elif indicator.startswith('item') and item:
136 140 slice = 'end'
137 141 if '-' in indicator:
138 142 wid = int(indicator.split('-')[1])
139 143 elif '+' in indicator:
140 144 slice = 'beginning'
141 145 wid = int(indicator.split('+')[1])
142 146 else:
143 147 wid = 20
144 148 if slice == 'end':
145 149 add = encoding.trim(item, wid, leftside=True)
146 150 else:
147 151 add = encoding.trim(item, wid)
148 152 add += (wid - encoding.colwidth(add)) * ' '
149 153 elif indicator == 'bar':
150 154 add = ''
151 155 needprogress = True
152 156 elif indicator == 'unit' and unit:
153 157 add = unit
154 158 elif indicator == 'estimate':
155 159 add = self.estimate(topic, pos, total, now)
156 160 elif indicator == 'speed':
157 161 add = self.speed(topic, pos, unit, now)
158 162 if not needprogress:
159 163 head = spacejoin(head, add)
160 164 else:
161 165 tail = spacejoin(tail, add)
162 166 if needprogress:
163 167 used = 0
164 168 if head:
165 169 used += encoding.colwidth(head) + 1
166 170 if tail:
167 171 used += encoding.colwidth(tail) + 1
168 172 progwidth = termwidth - used - 3
169 173 if total and pos <= total:
170 174 amt = pos * progwidth // total
171 175 bar = '=' * (amt - 1)
172 176 if amt > 0:
173 177 bar += '>'
174 178 bar += ' ' * (progwidth - amt)
175 179 else:
176 180 progwidth -= 3
177 181 self.indetcount += 1
178 182 # mod the count by twice the width so we can make the
179 183 # cursor bounce between the right and left sides
180 184 amt = self.indetcount % (2 * progwidth)
181 185 amt -= progwidth
182 186 bar = (' ' * int(progwidth - abs(amt)) + '<=>' +
183 187 ' ' * int(abs(amt)))
184 188 prog = ''.join(('[', bar , ']'))
185 189 out = spacejoin(head, prog, tail)
186 190 else:
187 191 out = spacejoin(head, tail)
188 192 sys.stderr.write('\r' + encoding.trim(out, termwidth))
189 193 self.lasttopic = topic
190 194 sys.stderr.flush()
191 195
192 196 def clear(self):
193 197 if not shouldprint(self.ui):
194 198 return
195 199 sys.stderr.write('\r%s\r' % (' ' * self.width()))
196 200
197 201 def complete(self):
198 202 if not shouldprint(self.ui):
199 203 return
200 204 if self.ui.configbool('progress', 'clear-complete', default=True):
201 205 self.clear()
202 206 else:
203 207 sys.stderr.write('\n')
204 208 sys.stderr.flush()
205 209
206 210 def width(self):
207 211 tw = self.ui.termwidth()
208 212 return min(int(self.ui.config('progress', 'width', default=tw)), tw)
209 213
210 214 def estimate(self, topic, pos, total, now):
211 215 if total is None:
212 216 return ''
213 217 initialpos = self.startvals[topic]
214 218 target = total - initialpos
215 219 delta = pos - initialpos
216 220 if delta > 0:
217 221 elapsed = now - self.starttimes[topic]
218 222 if elapsed > float(
219 223 self.ui.config('progress', 'estimate', default=2)):
220 224 seconds = (elapsed * (target - delta)) // delta + 1
221 225 return fmtremaining(seconds)
222 226 return ''
223 227
224 228 def speed(self, topic, pos, unit, now):
225 229 initialpos = self.startvals[topic]
226 230 delta = pos - initialpos
227 231 elapsed = now - self.starttimes[topic]
228 232 if elapsed > float(
229 233 self.ui.config('progress', 'estimate', default=2)):
230 234 return _('%d %s/sec') % (delta / elapsed, unit)
231 235 return ''
232 236
233 237 def _oktoprint(self, now):
234 238 '''Check if conditions are met to print - e.g. changedelay elapsed'''
235 239 if (self.lasttopic is None # first time we printed
236 240 # not a topic change
237 241 or self.curtopic == self.lasttopic
238 242 # it's been long enough we should print anyway
239 243 or now - self.lastprint >= self.changedelay):
240 244 return True
241 245 else:
242 246 return False
243 247
244 248 def progress(self, topic, pos, item='', unit='', total=None):
245 249 now = time.time()
246 250 self._refreshlock.acquire()
247 251 try:
248 252 if pos is None:
249 253 self.starttimes.pop(topic, None)
250 254 self.startvals.pop(topic, None)
251 255 self.topicstates.pop(topic, None)
252 256 # reset the progress bar if this is the outermost topic
253 257 if self.topics and self.topics[0] == topic and self.printed:
254 258 self.complete()
255 259 self.resetstate()
256 260 # truncate the list of topics assuming all topics within
257 261 # this one are also closed
258 262 if topic in self.topics:
259 263 self.topics = self.topics[:self.topics.index(topic)]
260 264 # reset the last topic to the one we just unwound to,
261 265 # so that higher-level topics will be stickier than
262 266 # lower-level topics
263 267 if self.topics:
264 268 self.lasttopic = self.topics[-1]
265 269 else:
266 270 self.lasttopic = None
267 271 else:
268 272 if topic not in self.topics:
269 273 self.starttimes[topic] = now
270 274 self.startvals[topic] = pos
271 275 self.topics.append(topic)
272 276 self.topicstates[topic] = pos, item, unit, total
273 277 self.curtopic = topic
274 278 if now - self.lastprint >= self.refresh and self.topics:
275 279 if self._oktoprint(now):
276 280 self.lastprint = now
277 281 self.show(now, topic, *self.topicstates[topic])
278 282 finally:
279 283 self._refreshlock.release()
280 284
281 285 _singleton = None
282 286
283 287 def uisetup(ui):
284 288 global _singleton
285 289 class progressui(ui.__class__):
286 290 _progbar = None
287 291
288 292 def _quiet(self):
289 293 return self.debugflag or self.quiet
290 294
291 295 def progress(self, *args, **opts):
292 296 if not self._quiet():
293 297 self._progbar.progress(*args, **opts)
294 298 return super(progressui, self).progress(*args, **opts)
295 299
296 300 def write(self, *args, **opts):
297 301 if not self._quiet() and self._progbar.printed:
298 302 self._progbar.clear()
299 303 return super(progressui, self).write(*args, **opts)
300 304
301 305 def write_err(self, *args, **opts):
302 306 if not self._quiet() and self._progbar.printed:
303 307 self._progbar.clear()
304 308 return super(progressui, self).write_err(*args, **opts)
305 309
306 310 # Apps that derive a class from ui.ui() can use
307 311 # setconfig('progress', 'disable', 'True') to disable this extension
308 312 if ui.configbool('progress', 'disable'):
309 313 return
310 314 if shouldprint(ui) and not ui.debugflag and not ui.quiet:
311 315 ui.__class__ = progressui
312 316 # we instantiate one globally shared progress bar to avoid
313 317 # competing progress bars when multiple UI objects get created
314 318 if not progressui._progbar:
315 319 if _singleton is None:
316 320 _singleton = progbar(ui)
317 321 progressui._progbar = _singleton
318 322
319 323 def reposetup(ui, repo):
320 324 uisetup(repo.ui)
@@ -1,115 +1,119 b''
1 1 # Copyright (C) 2006 - Marco Barisione <marco@barisione.org>
2 2 #
3 3 # This is a small extension for Mercurial (http://mercurial.selenic.com/)
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
27 27 from mercurial import util, commands, cmdutil, scmutil
28 28 from mercurial.i18n import _
29 29 import os
30 30
31 31 cmdtable = {}
32 32 command = cmdutil.command(cmdtable)
33 # Note for extension authors: ONLY specify testedwith = 'internal' for
34 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
35 # be specifying the version(s) of Mercurial they are tested with, or
36 # leave the attribute unspecified.
33 37 testedwith = 'internal'
34 38
35 39 @command('purge|clean',
36 40 [('a', 'abort-on-err', None, _('abort if an error occurs')),
37 41 ('', 'all', None, _('purge ignored files too')),
38 42 ('', 'dirs', None, _('purge empty directories')),
39 43 ('', 'files', None, _('purge files')),
40 44 ('p', 'print', None, _('print filenames instead of deleting them')),
41 45 ('0', 'print0', None, _('end filenames with NUL, for use with xargs'
42 46 ' (implies -p/--print)')),
43 47 ] + commands.walkopts,
44 48 _('hg purge [OPTION]... [DIR]...'))
45 49 def purge(ui, repo, *dirs, **opts):
46 50 '''removes files not tracked by Mercurial
47 51
48 52 Delete files not known to Mercurial. This is useful to test local
49 53 and uncommitted changes in an otherwise-clean source tree.
50 54
51 55 This means that purge will delete the following by default:
52 56
53 57 - Unknown files: files marked with "?" by :hg:`status`
54 58 - Empty directories: in fact Mercurial ignores directories unless
55 59 they contain files under source control management
56 60
57 61 But it will leave untouched:
58 62
59 63 - Modified and unmodified tracked files
60 64 - Ignored files (unless --all is specified)
61 65 - New files added to the repository (with :hg:`add`)
62 66
63 67 The --files and --dirs options can be used to direct purge to delete
64 68 only files, only directories, or both. If neither option is given,
65 69 both will be deleted.
66 70
67 71 If directories are given on the command line, only files in these
68 72 directories are considered.
69 73
70 74 Be careful with purge, as you could irreversibly delete some files
71 75 you forgot to add to the repository. If you only want to print the
72 76 list of files that this program would delete, use the --print
73 77 option.
74 78 '''
75 79 act = not opts['print']
76 80 eol = '\n'
77 81 if opts['print0']:
78 82 eol = '\0'
79 83 act = False # --print0 implies --print
80 84 removefiles = opts['files']
81 85 removedirs = opts['dirs']
82 86 if not removefiles and not removedirs:
83 87 removefiles = True
84 88 removedirs = True
85 89
86 90 def remove(remove_func, name):
87 91 if act:
88 92 try:
89 93 remove_func(repo.wjoin(name))
90 94 except OSError:
91 95 m = _('%s cannot be removed') % name
92 96 if opts['abort_on_err']:
93 97 raise util.Abort(m)
94 98 ui.warn(_('warning: %s\n') % m)
95 99 else:
96 100 ui.write('%s%s' % (name, eol))
97 101
98 102 match = scmutil.match(repo[None], dirs, opts)
99 103 if removedirs:
100 104 directories = []
101 105 match.explicitdir = match.traversedir = directories.append
102 106 status = repo.status(match=match, ignored=opts['all'], unknown=True)
103 107
104 108 if removefiles:
105 109 for f in sorted(status.unknown + status.ignored):
106 110 if act:
107 111 ui.note(_('removing file %s\n') % f)
108 112 remove(util.unlink, f)
109 113
110 114 if removedirs:
111 115 for f in sorted(directories, reverse=True):
112 116 if match(f) and not os.listdir(repo.wjoin(f)):
113 117 if act:
114 118 ui.note(_('removing directory %s\n') % f)
115 119 remove(os.rmdir, f)
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