##// END OF EJS Templates
make_file: always return a fresh file handle that can be closed...
Dan Villiom Podlaski Christiansen -
r13121:bf763946 default
parent child Browse files
Show More
@@ -1,1379 +1,1380 b''
1 1 # cmdutil.py - help for command processing in mercurial
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 from node import hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, errno, re, glob, tempfile
11 11 import util, templater, patch, error, encoding, templatekw
12 12 import match as matchmod
13 13 import similar, revset, subrepo
14 14
15 15 revrangesep = ':'
16 16
17 17 def parsealiases(cmd):
18 18 return cmd.lstrip("^").split("|")
19 19
20 20 def findpossible(cmd, table, strict=False):
21 21 """
22 22 Return cmd -> (aliases, command table entry)
23 23 for each matching command.
24 24 Return debug commands (or their aliases) only if no normal command matches.
25 25 """
26 26 choice = {}
27 27 debugchoice = {}
28 28 for e in table.keys():
29 29 aliases = parsealiases(e)
30 30 found = None
31 31 if cmd in aliases:
32 32 found = cmd
33 33 elif not strict:
34 34 for a in aliases:
35 35 if a.startswith(cmd):
36 36 found = a
37 37 break
38 38 if found is not None:
39 39 if aliases[0].startswith("debug") or found.startswith("debug"):
40 40 debugchoice[found] = (aliases, table[e])
41 41 else:
42 42 choice[found] = (aliases, table[e])
43 43
44 44 if not choice and debugchoice:
45 45 choice = debugchoice
46 46
47 47 return choice
48 48
49 49 def findcmd(cmd, table, strict=True):
50 50 """Return (aliases, command table entry) for command string."""
51 51 choice = findpossible(cmd, table, strict)
52 52
53 53 if cmd in choice:
54 54 return choice[cmd]
55 55
56 56 if len(choice) > 1:
57 57 clist = choice.keys()
58 58 clist.sort()
59 59 raise error.AmbiguousCommand(cmd, clist)
60 60
61 61 if choice:
62 62 return choice.values()[0]
63 63
64 64 raise error.UnknownCommand(cmd)
65 65
66 66 def findrepo(p):
67 67 while not os.path.isdir(os.path.join(p, ".hg")):
68 68 oldp, p = p, os.path.dirname(p)
69 69 if p == oldp:
70 70 return None
71 71
72 72 return p
73 73
74 74 def bail_if_changed(repo):
75 75 if repo.dirstate.parents()[1] != nullid:
76 76 raise util.Abort(_('outstanding uncommitted merge'))
77 77 modified, added, removed, deleted = repo.status()[:4]
78 78 if modified or added or removed or deleted:
79 79 raise util.Abort(_("outstanding uncommitted changes"))
80 80
81 81 def logmessage(opts):
82 82 """ get the log message according to -m and -l option """
83 83 message = opts.get('message')
84 84 logfile = opts.get('logfile')
85 85
86 86 if message and logfile:
87 87 raise util.Abort(_('options --message and --logfile are mutually '
88 88 'exclusive'))
89 89 if not message and logfile:
90 90 try:
91 91 if logfile == '-':
92 92 message = sys.stdin.read()
93 93 else:
94 94 message = open(logfile).read()
95 95 except IOError, inst:
96 96 raise util.Abort(_("can't read commit message '%s': %s") %
97 97 (logfile, inst.strerror))
98 98 return message
99 99
100 100 def loglimit(opts):
101 101 """get the log limit according to option -l/--limit"""
102 102 limit = opts.get('limit')
103 103 if limit:
104 104 try:
105 105 limit = int(limit)
106 106 except ValueError:
107 107 raise util.Abort(_('limit must be a positive integer'))
108 108 if limit <= 0:
109 109 raise util.Abort(_('limit must be positive'))
110 110 else:
111 111 limit = None
112 112 return limit
113 113
114 114 def revsingle(repo, revspec, default='.'):
115 115 if not revspec:
116 116 return repo[default]
117 117
118 118 l = revrange(repo, [revspec])
119 119 if len(l) < 1:
120 120 raise util.Abort(_('empty revision set'))
121 121 return repo[l[-1]]
122 122
123 123 def revpair(repo, revs):
124 124 if not revs:
125 125 return repo.dirstate.parents()[0], None
126 126
127 127 l = revrange(repo, revs)
128 128
129 129 if len(l) == 0:
130 130 return repo.dirstate.parents()[0], None
131 131
132 132 if len(l) == 1:
133 133 return repo.lookup(l[0]), None
134 134
135 135 return repo.lookup(l[0]), repo.lookup(l[-1])
136 136
137 137 def revrange(repo, revs):
138 138 """Yield revision as strings from a list of revision specifications."""
139 139
140 140 def revfix(repo, val, defval):
141 141 if not val and val != 0 and defval is not None:
142 142 return defval
143 143 return repo.changelog.rev(repo.lookup(val))
144 144
145 145 seen, l = set(), []
146 146 for spec in revs:
147 147 # attempt to parse old-style ranges first to deal with
148 148 # things like old-tag which contain query metacharacters
149 149 try:
150 150 if isinstance(spec, int):
151 151 seen.add(spec)
152 152 l.append(spec)
153 153 continue
154 154
155 155 if revrangesep in spec:
156 156 start, end = spec.split(revrangesep, 1)
157 157 start = revfix(repo, start, 0)
158 158 end = revfix(repo, end, len(repo) - 1)
159 159 step = start > end and -1 or 1
160 160 for rev in xrange(start, end + step, step):
161 161 if rev in seen:
162 162 continue
163 163 seen.add(rev)
164 164 l.append(rev)
165 165 continue
166 166 elif spec and spec in repo: # single unquoted rev
167 167 rev = revfix(repo, spec, None)
168 168 if rev in seen:
169 169 continue
170 170 seen.add(rev)
171 171 l.append(rev)
172 172 continue
173 173 except error.RepoLookupError:
174 174 pass
175 175
176 176 # fall through to new-style queries if old-style fails
177 177 m = revset.match(spec)
178 178 for r in m(repo, range(len(repo))):
179 179 if r not in seen:
180 180 l.append(r)
181 181 seen.update(l)
182 182
183 183 return l
184 184
185 185 def make_filename(repo, pat, node,
186 186 total=None, seqno=None, revwidth=None, pathname=None):
187 187 node_expander = {
188 188 'H': lambda: hex(node),
189 189 'R': lambda: str(repo.changelog.rev(node)),
190 190 'h': lambda: short(node),
191 191 }
192 192 expander = {
193 193 '%': lambda: '%',
194 194 'b': lambda: os.path.basename(repo.root),
195 195 }
196 196
197 197 try:
198 198 if node:
199 199 expander.update(node_expander)
200 200 if node:
201 201 expander['r'] = (lambda:
202 202 str(repo.changelog.rev(node)).zfill(revwidth or 0))
203 203 if total is not None:
204 204 expander['N'] = lambda: str(total)
205 205 if seqno is not None:
206 206 expander['n'] = lambda: str(seqno)
207 207 if total is not None and seqno is not None:
208 208 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
209 209 if pathname is not None:
210 210 expander['s'] = lambda: os.path.basename(pathname)
211 211 expander['d'] = lambda: os.path.dirname(pathname) or '.'
212 212 expander['p'] = lambda: pathname
213 213
214 214 newname = []
215 215 patlen = len(pat)
216 216 i = 0
217 217 while i < patlen:
218 218 c = pat[i]
219 219 if c == '%':
220 220 i += 1
221 221 c = pat[i]
222 222 c = expander[c]()
223 223 newname.append(c)
224 224 i += 1
225 225 return ''.join(newname)
226 226 except KeyError, inst:
227 227 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
228 228 inst.args[0])
229 229
230 230 def make_file(repo, pat, node=None,
231 231 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
232 232
233 233 writable = 'w' in mode or 'a' in mode
234 234
235 235 if not pat or pat == '-':
236 return writable and sys.stdout or sys.stdin
236 fp = writable and sys.stdout or sys.stdin
237 return os.fdopen(os.dup(fp.fileno()), mode)
237 238 if hasattr(pat, 'write') and writable:
238 239 return pat
239 240 if hasattr(pat, 'read') and 'r' in mode:
240 241 return pat
241 242 return open(make_filename(repo, pat, node, total, seqno, revwidth,
242 243 pathname),
243 244 mode)
244 245
245 246 def expandpats(pats):
246 247 if not util.expandglobs:
247 248 return list(pats)
248 249 ret = []
249 250 for p in pats:
250 251 kind, name = matchmod._patsplit(p, None)
251 252 if kind is None:
252 253 try:
253 254 globbed = glob.glob(name)
254 255 except re.error:
255 256 globbed = [name]
256 257 if globbed:
257 258 ret.extend(globbed)
258 259 continue
259 260 ret.append(p)
260 261 return ret
261 262
262 263 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
263 264 if not globbed and default == 'relpath':
264 265 pats = expandpats(pats or [])
265 266 m = matchmod.match(repo.root, repo.getcwd(), pats,
266 267 opts.get('include'), opts.get('exclude'), default,
267 268 auditor=repo.auditor)
268 269 def badfn(f, msg):
269 270 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
270 271 m.bad = badfn
271 272 return m
272 273
273 274 def matchall(repo):
274 275 return matchmod.always(repo.root, repo.getcwd())
275 276
276 277 def matchfiles(repo, files):
277 278 return matchmod.exact(repo.root, repo.getcwd(), files)
278 279
279 280 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
280 281 if dry_run is None:
281 282 dry_run = opts.get('dry_run')
282 283 if similarity is None:
283 284 similarity = float(opts.get('similarity') or 0)
284 285 # we'd use status here, except handling of symlinks and ignore is tricky
285 286 added, unknown, deleted, removed = [], [], [], []
286 287 audit_path = util.path_auditor(repo.root)
287 288 m = match(repo, pats, opts)
288 289 for abs in repo.walk(m):
289 290 target = repo.wjoin(abs)
290 291 good = True
291 292 try:
292 293 audit_path(abs)
293 294 except:
294 295 good = False
295 296 rel = m.rel(abs)
296 297 exact = m.exact(abs)
297 298 if good and abs not in repo.dirstate:
298 299 unknown.append(abs)
299 300 if repo.ui.verbose or not exact:
300 301 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
301 302 elif repo.dirstate[abs] != 'r' and (not good or not os.path.lexists(target)
302 303 or (os.path.isdir(target) and not os.path.islink(target))):
303 304 deleted.append(abs)
304 305 if repo.ui.verbose or not exact:
305 306 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
306 307 # for finding renames
307 308 elif repo.dirstate[abs] == 'r':
308 309 removed.append(abs)
309 310 elif repo.dirstate[abs] == 'a':
310 311 added.append(abs)
311 312 copies = {}
312 313 if similarity > 0:
313 314 for old, new, score in similar.findrenames(repo,
314 315 added + unknown, removed + deleted, similarity):
315 316 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
316 317 repo.ui.status(_('recording removal of %s as rename to %s '
317 318 '(%d%% similar)\n') %
318 319 (m.rel(old), m.rel(new), score * 100))
319 320 copies[new] = old
320 321
321 322 if not dry_run:
322 323 wctx = repo[None]
323 324 wlock = repo.wlock()
324 325 try:
325 326 wctx.remove(deleted)
326 327 wctx.add(unknown)
327 328 for new, old in copies.iteritems():
328 329 wctx.copy(old, new)
329 330 finally:
330 331 wlock.release()
331 332
332 333 def updatedir(ui, repo, patches, similarity=0):
333 334 '''Update dirstate after patch application according to metadata'''
334 335 if not patches:
335 336 return
336 337 copies = []
337 338 removes = set()
338 339 cfiles = patches.keys()
339 340 cwd = repo.getcwd()
340 341 if cwd:
341 342 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
342 343 for f in patches:
343 344 gp = patches[f]
344 345 if not gp:
345 346 continue
346 347 if gp.op == 'RENAME':
347 348 copies.append((gp.oldpath, gp.path))
348 349 removes.add(gp.oldpath)
349 350 elif gp.op == 'COPY':
350 351 copies.append((gp.oldpath, gp.path))
351 352 elif gp.op == 'DELETE':
352 353 removes.add(gp.path)
353 354
354 355 wctx = repo[None]
355 356 for src, dst in copies:
356 357 dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd)
357 358 if (not similarity) and removes:
358 359 wctx.remove(sorted(removes), True)
359 360
360 361 for f in patches:
361 362 gp = patches[f]
362 363 if gp and gp.mode:
363 364 islink, isexec = gp.mode
364 365 dst = repo.wjoin(gp.path)
365 366 # patch won't create empty files
366 367 if gp.op == 'ADD' and not os.path.lexists(dst):
367 368 flags = (isexec and 'x' or '') + (islink and 'l' or '')
368 369 repo.wwrite(gp.path, '', flags)
369 370 util.set_flags(dst, islink, isexec)
370 371 addremove(repo, cfiles, similarity=similarity)
371 372 files = patches.keys()
372 373 files.extend([r for r in removes if r not in files])
373 374 return sorted(files)
374 375
375 376 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
376 377 """Update the dirstate to reflect the intent of copying src to dst. For
377 378 different reasons it might not end with dst being marked as copied from src.
378 379 """
379 380 origsrc = repo.dirstate.copied(src) or src
380 381 if dst == origsrc: # copying back a copy?
381 382 if repo.dirstate[dst] not in 'mn' and not dryrun:
382 383 repo.dirstate.normallookup(dst)
383 384 else:
384 385 if repo.dirstate[origsrc] == 'a' and origsrc == src:
385 386 if not ui.quiet:
386 387 ui.warn(_("%s has not been committed yet, so no copy "
387 388 "data will be stored for %s.\n")
388 389 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
389 390 if repo.dirstate[dst] in '?r' and not dryrun:
390 391 wctx.add([dst])
391 392 elif not dryrun:
392 393 wctx.copy(origsrc, dst)
393 394
394 395 def copy(ui, repo, pats, opts, rename=False):
395 396 # called with the repo lock held
396 397 #
397 398 # hgsep => pathname that uses "/" to separate directories
398 399 # ossep => pathname that uses os.sep to separate directories
399 400 cwd = repo.getcwd()
400 401 targets = {}
401 402 after = opts.get("after")
402 403 dryrun = opts.get("dry_run")
403 404 wctx = repo[None]
404 405
405 406 def walkpat(pat):
406 407 srcs = []
407 408 badstates = after and '?' or '?r'
408 409 m = match(repo, [pat], opts, globbed=True)
409 410 for abs in repo.walk(m):
410 411 state = repo.dirstate[abs]
411 412 rel = m.rel(abs)
412 413 exact = m.exact(abs)
413 414 if state in badstates:
414 415 if exact and state == '?':
415 416 ui.warn(_('%s: not copying - file is not managed\n') % rel)
416 417 if exact and state == 'r':
417 418 ui.warn(_('%s: not copying - file has been marked for'
418 419 ' remove\n') % rel)
419 420 continue
420 421 # abs: hgsep
421 422 # rel: ossep
422 423 srcs.append((abs, rel, exact))
423 424 return srcs
424 425
425 426 # abssrc: hgsep
426 427 # relsrc: ossep
427 428 # otarget: ossep
428 429 def copyfile(abssrc, relsrc, otarget, exact):
429 430 abstarget = util.canonpath(repo.root, cwd, otarget)
430 431 reltarget = repo.pathto(abstarget, cwd)
431 432 target = repo.wjoin(abstarget)
432 433 src = repo.wjoin(abssrc)
433 434 state = repo.dirstate[abstarget]
434 435
435 436 # check for collisions
436 437 prevsrc = targets.get(abstarget)
437 438 if prevsrc is not None:
438 439 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
439 440 (reltarget, repo.pathto(abssrc, cwd),
440 441 repo.pathto(prevsrc, cwd)))
441 442 return
442 443
443 444 # check for overwrites
444 445 exists = os.path.lexists(target)
445 446 if not after and exists or after and state in 'mn':
446 447 if not opts['force']:
447 448 ui.warn(_('%s: not overwriting - file exists\n') %
448 449 reltarget)
449 450 return
450 451
451 452 if after:
452 453 if not exists:
453 454 if rename:
454 455 ui.warn(_('%s: not recording move - %s does not exist\n') %
455 456 (relsrc, reltarget))
456 457 else:
457 458 ui.warn(_('%s: not recording copy - %s does not exist\n') %
458 459 (relsrc, reltarget))
459 460 return
460 461 elif not dryrun:
461 462 try:
462 463 if exists:
463 464 os.unlink(target)
464 465 targetdir = os.path.dirname(target) or '.'
465 466 if not os.path.isdir(targetdir):
466 467 os.makedirs(targetdir)
467 468 util.copyfile(src, target)
468 469 except IOError, inst:
469 470 if inst.errno == errno.ENOENT:
470 471 ui.warn(_('%s: deleted in working copy\n') % relsrc)
471 472 else:
472 473 ui.warn(_('%s: cannot copy - %s\n') %
473 474 (relsrc, inst.strerror))
474 475 return True # report a failure
475 476
476 477 if ui.verbose or not exact:
477 478 if rename:
478 479 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
479 480 else:
480 481 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
481 482
482 483 targets[abstarget] = abssrc
483 484
484 485 # fix up dirstate
485 486 dirstatecopy(ui, repo, wctx, abssrc, abstarget, dryrun=dryrun, cwd=cwd)
486 487 if rename and not dryrun:
487 488 wctx.remove([abssrc], not after)
488 489
489 490 # pat: ossep
490 491 # dest ossep
491 492 # srcs: list of (hgsep, hgsep, ossep, bool)
492 493 # return: function that takes hgsep and returns ossep
493 494 def targetpathfn(pat, dest, srcs):
494 495 if os.path.isdir(pat):
495 496 abspfx = util.canonpath(repo.root, cwd, pat)
496 497 abspfx = util.localpath(abspfx)
497 498 if destdirexists:
498 499 striplen = len(os.path.split(abspfx)[0])
499 500 else:
500 501 striplen = len(abspfx)
501 502 if striplen:
502 503 striplen += len(os.sep)
503 504 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
504 505 elif destdirexists:
505 506 res = lambda p: os.path.join(dest,
506 507 os.path.basename(util.localpath(p)))
507 508 else:
508 509 res = lambda p: dest
509 510 return res
510 511
511 512 # pat: ossep
512 513 # dest ossep
513 514 # srcs: list of (hgsep, hgsep, ossep, bool)
514 515 # return: function that takes hgsep and returns ossep
515 516 def targetpathafterfn(pat, dest, srcs):
516 517 if matchmod.patkind(pat):
517 518 # a mercurial pattern
518 519 res = lambda p: os.path.join(dest,
519 520 os.path.basename(util.localpath(p)))
520 521 else:
521 522 abspfx = util.canonpath(repo.root, cwd, pat)
522 523 if len(abspfx) < len(srcs[0][0]):
523 524 # A directory. Either the target path contains the last
524 525 # component of the source path or it does not.
525 526 def evalpath(striplen):
526 527 score = 0
527 528 for s in srcs:
528 529 t = os.path.join(dest, util.localpath(s[0])[striplen:])
529 530 if os.path.lexists(t):
530 531 score += 1
531 532 return score
532 533
533 534 abspfx = util.localpath(abspfx)
534 535 striplen = len(abspfx)
535 536 if striplen:
536 537 striplen += len(os.sep)
537 538 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
538 539 score = evalpath(striplen)
539 540 striplen1 = len(os.path.split(abspfx)[0])
540 541 if striplen1:
541 542 striplen1 += len(os.sep)
542 543 if evalpath(striplen1) > score:
543 544 striplen = striplen1
544 545 res = lambda p: os.path.join(dest,
545 546 util.localpath(p)[striplen:])
546 547 else:
547 548 # a file
548 549 if destdirexists:
549 550 res = lambda p: os.path.join(dest,
550 551 os.path.basename(util.localpath(p)))
551 552 else:
552 553 res = lambda p: dest
553 554 return res
554 555
555 556
556 557 pats = expandpats(pats)
557 558 if not pats:
558 559 raise util.Abort(_('no source or destination specified'))
559 560 if len(pats) == 1:
560 561 raise util.Abort(_('no destination specified'))
561 562 dest = pats.pop()
562 563 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
563 564 if not destdirexists:
564 565 if len(pats) > 1 or matchmod.patkind(pats[0]):
565 566 raise util.Abort(_('with multiple sources, destination must be an '
566 567 'existing directory'))
567 568 if util.endswithsep(dest):
568 569 raise util.Abort(_('destination %s is not a directory') % dest)
569 570
570 571 tfn = targetpathfn
571 572 if after:
572 573 tfn = targetpathafterfn
573 574 copylist = []
574 575 for pat in pats:
575 576 srcs = walkpat(pat)
576 577 if not srcs:
577 578 continue
578 579 copylist.append((tfn(pat, dest, srcs), srcs))
579 580 if not copylist:
580 581 raise util.Abort(_('no files to copy'))
581 582
582 583 errors = 0
583 584 for targetpath, srcs in copylist:
584 585 for abssrc, relsrc, exact in srcs:
585 586 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
586 587 errors += 1
587 588
588 589 if errors:
589 590 ui.warn(_('(consider using --after)\n'))
590 591
591 592 return errors != 0
592 593
593 594 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
594 595 runargs=None, appendpid=False):
595 596 '''Run a command as a service.'''
596 597
597 598 if opts['daemon'] and not opts['daemon_pipefds']:
598 599 # Signal child process startup with file removal
599 600 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
600 601 os.close(lockfd)
601 602 try:
602 603 if not runargs:
603 604 runargs = util.hgcmd() + sys.argv[1:]
604 605 runargs.append('--daemon-pipefds=%s' % lockpath)
605 606 # Don't pass --cwd to the child process, because we've already
606 607 # changed directory.
607 608 for i in xrange(1, len(runargs)):
608 609 if runargs[i].startswith('--cwd='):
609 610 del runargs[i]
610 611 break
611 612 elif runargs[i].startswith('--cwd'):
612 613 del runargs[i:i + 2]
613 614 break
614 615 def condfn():
615 616 return not os.path.exists(lockpath)
616 617 pid = util.rundetached(runargs, condfn)
617 618 if pid < 0:
618 619 raise util.Abort(_('child process failed to start'))
619 620 finally:
620 621 try:
621 622 os.unlink(lockpath)
622 623 except OSError, e:
623 624 if e.errno != errno.ENOENT:
624 625 raise
625 626 if parentfn:
626 627 return parentfn(pid)
627 628 else:
628 629 return
629 630
630 631 if initfn:
631 632 initfn()
632 633
633 634 if opts['pid_file']:
634 635 mode = appendpid and 'a' or 'w'
635 636 fp = open(opts['pid_file'], mode)
636 637 fp.write(str(os.getpid()) + '\n')
637 638 fp.close()
638 639
639 640 if opts['daemon_pipefds']:
640 641 lockpath = opts['daemon_pipefds']
641 642 try:
642 643 os.setsid()
643 644 except AttributeError:
644 645 pass
645 646 os.unlink(lockpath)
646 647 util.hidewindow()
647 648 sys.stdout.flush()
648 649 sys.stderr.flush()
649 650
650 651 nullfd = os.open(util.nulldev, os.O_RDWR)
651 652 logfilefd = nullfd
652 653 if logfile:
653 654 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
654 655 os.dup2(nullfd, 0)
655 656 os.dup2(logfilefd, 1)
656 657 os.dup2(logfilefd, 2)
657 658 if nullfd not in (0, 1, 2):
658 659 os.close(nullfd)
659 660 if logfile and logfilefd not in (0, 1, 2):
660 661 os.close(logfilefd)
661 662
662 663 if runfn:
663 664 return runfn()
664 665
665 666 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
666 667 opts=None):
667 668 '''export changesets as hg patches.'''
668 669
669 670 total = len(revs)
670 671 revwidth = max([len(str(rev)) for rev in revs])
671 672
672 673 def single(rev, seqno, fp):
673 674 ctx = repo[rev]
674 675 node = ctx.node()
675 676 parents = [p.node() for p in ctx.parents() if p]
676 677 branch = ctx.branch()
677 678 if switch_parent:
678 679 parents.reverse()
679 680 prev = (parents and parents[0]) or nullid
680 681
681 682 if not fp:
682 683 fp = make_file(repo, template, node, total=total, seqno=seqno,
683 684 revwidth=revwidth, mode='ab')
684 685 if fp != sys.stdout and hasattr(fp, 'name'):
685 686 repo.ui.note("%s\n" % fp.name)
686 687
687 688 fp.write("# HG changeset patch\n")
688 689 fp.write("# User %s\n" % ctx.user())
689 690 fp.write("# Date %d %d\n" % ctx.date())
690 691 if branch and branch != 'default':
691 692 fp.write("# Branch %s\n" % branch)
692 693 fp.write("# Node ID %s\n" % hex(node))
693 694 fp.write("# Parent %s\n" % hex(prev))
694 695 if len(parents) > 1:
695 696 fp.write("# Parent %s\n" % hex(parents[1]))
696 697 fp.write(ctx.description().rstrip())
697 698 fp.write("\n\n")
698 699
699 700 for chunk in patch.diff(repo, prev, node, opts=opts):
700 701 fp.write(chunk)
701 702
702 703 fp.flush()
703 704
704 705 for seqno, rev in enumerate(revs):
705 706 single(rev, seqno + 1, fp)
706 707
707 708 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
708 709 changes=None, stat=False, fp=None, prefix='',
709 710 listsubrepos=False):
710 711 '''show diff or diffstat.'''
711 712 if fp is None:
712 713 write = ui.write
713 714 else:
714 715 def write(s, **kw):
715 716 fp.write(s)
716 717
717 718 if stat:
718 719 diffopts = diffopts.copy(context=0)
719 720 width = 80
720 721 if not ui.plain():
721 722 width = ui.termwidth()
722 723 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
723 724 prefix=prefix)
724 725 for chunk, label in patch.diffstatui(util.iterlines(chunks),
725 726 width=width,
726 727 git=diffopts.git):
727 728 write(chunk, label=label)
728 729 else:
729 730 for chunk, label in patch.diffui(repo, node1, node2, match,
730 731 changes, diffopts, prefix=prefix):
731 732 write(chunk, label=label)
732 733
733 734 if listsubrepos:
734 735 ctx1 = repo[node1]
735 736 ctx2 = repo[node2]
736 737 for subpath, sub in subrepo.itersubrepos(ctx1, ctx2):
737 738 if node2 is not None:
738 739 node2 = ctx2.substate[subpath][1]
739 740 submatch = matchmod.narrowmatcher(subpath, match)
740 741 sub.diff(diffopts, node2, submatch, changes=changes,
741 742 stat=stat, fp=fp, prefix=prefix)
742 743
743 744 class changeset_printer(object):
744 745 '''show changeset information when templating not requested.'''
745 746
746 747 def __init__(self, ui, repo, patch, diffopts, buffered):
747 748 self.ui = ui
748 749 self.repo = repo
749 750 self.buffered = buffered
750 751 self.patch = patch
751 752 self.diffopts = diffopts
752 753 self.header = {}
753 754 self.hunk = {}
754 755 self.lastheader = None
755 756 self.footer = None
756 757
757 758 def flush(self, rev):
758 759 if rev in self.header:
759 760 h = self.header[rev]
760 761 if h != self.lastheader:
761 762 self.lastheader = h
762 763 self.ui.write(h)
763 764 del self.header[rev]
764 765 if rev in self.hunk:
765 766 self.ui.write(self.hunk[rev])
766 767 del self.hunk[rev]
767 768 return 1
768 769 return 0
769 770
770 771 def close(self):
771 772 if self.footer:
772 773 self.ui.write(self.footer)
773 774
774 775 def show(self, ctx, copies=None, matchfn=None, **props):
775 776 if self.buffered:
776 777 self.ui.pushbuffer()
777 778 self._show(ctx, copies, matchfn, props)
778 779 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
779 780 else:
780 781 self._show(ctx, copies, matchfn, props)
781 782
782 783 def _show(self, ctx, copies, matchfn, props):
783 784 '''show a single changeset or file revision'''
784 785 changenode = ctx.node()
785 786 rev = ctx.rev()
786 787
787 788 if self.ui.quiet:
788 789 self.ui.write("%d:%s\n" % (rev, short(changenode)),
789 790 label='log.node')
790 791 return
791 792
792 793 log = self.repo.changelog
793 794 date = util.datestr(ctx.date())
794 795
795 796 hexfunc = self.ui.debugflag and hex or short
796 797
797 798 parents = [(p, hexfunc(log.node(p)))
798 799 for p in self._meaningful_parentrevs(log, rev)]
799 800
800 801 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
801 802 label='log.changeset')
802 803
803 804 branch = ctx.branch()
804 805 # don't show the default branch name
805 806 if branch != 'default':
806 807 self.ui.write(_("branch: %s\n") % branch,
807 808 label='log.branch')
808 809 for tag in self.repo.nodetags(changenode):
809 810 self.ui.write(_("tag: %s\n") % tag,
810 811 label='log.tag')
811 812 for parent in parents:
812 813 self.ui.write(_("parent: %d:%s\n") % parent,
813 814 label='log.parent')
814 815
815 816 if self.ui.debugflag:
816 817 mnode = ctx.manifestnode()
817 818 self.ui.write(_("manifest: %d:%s\n") %
818 819 (self.repo.manifest.rev(mnode), hex(mnode)),
819 820 label='ui.debug log.manifest')
820 821 self.ui.write(_("user: %s\n") % ctx.user(),
821 822 label='log.user')
822 823 self.ui.write(_("date: %s\n") % date,
823 824 label='log.date')
824 825
825 826 if self.ui.debugflag:
826 827 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
827 828 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
828 829 files):
829 830 if value:
830 831 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
831 832 label='ui.debug log.files')
832 833 elif ctx.files() and self.ui.verbose:
833 834 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
834 835 label='ui.note log.files')
835 836 if copies and self.ui.verbose:
836 837 copies = ['%s (%s)' % c for c in copies]
837 838 self.ui.write(_("copies: %s\n") % ' '.join(copies),
838 839 label='ui.note log.copies')
839 840
840 841 extra = ctx.extra()
841 842 if extra and self.ui.debugflag:
842 843 for key, value in sorted(extra.items()):
843 844 self.ui.write(_("extra: %s=%s\n")
844 845 % (key, value.encode('string_escape')),
845 846 label='ui.debug log.extra')
846 847
847 848 description = ctx.description().strip()
848 849 if description:
849 850 if self.ui.verbose:
850 851 self.ui.write(_("description:\n"),
851 852 label='ui.note log.description')
852 853 self.ui.write(description,
853 854 label='ui.note log.description')
854 855 self.ui.write("\n\n")
855 856 else:
856 857 self.ui.write(_("summary: %s\n") %
857 858 description.splitlines()[0],
858 859 label='log.summary')
859 860 self.ui.write("\n")
860 861
861 862 self.showpatch(changenode, matchfn)
862 863
863 864 def showpatch(self, node, matchfn):
864 865 if not matchfn:
865 866 matchfn = self.patch
866 867 if matchfn:
867 868 stat = self.diffopts.get('stat')
868 869 diff = self.diffopts.get('patch')
869 870 diffopts = patch.diffopts(self.ui, self.diffopts)
870 871 prev = self.repo.changelog.parents(node)[0]
871 872 if stat:
872 873 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
873 874 match=matchfn, stat=True)
874 875 if diff:
875 876 if stat:
876 877 self.ui.write("\n")
877 878 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
878 879 match=matchfn, stat=False)
879 880 self.ui.write("\n")
880 881
881 882 def _meaningful_parentrevs(self, log, rev):
882 883 """Return list of meaningful (or all if debug) parentrevs for rev.
883 884
884 885 For merges (two non-nullrev revisions) both parents are meaningful.
885 886 Otherwise the first parent revision is considered meaningful if it
886 887 is not the preceding revision.
887 888 """
888 889 parents = log.parentrevs(rev)
889 890 if not self.ui.debugflag and parents[1] == nullrev:
890 891 if parents[0] >= rev - 1:
891 892 parents = []
892 893 else:
893 894 parents = [parents[0]]
894 895 return parents
895 896
896 897
897 898 class changeset_templater(changeset_printer):
898 899 '''format changeset information.'''
899 900
900 901 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
901 902 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
902 903 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
903 904 defaulttempl = {
904 905 'parent': '{rev}:{node|formatnode} ',
905 906 'manifest': '{rev}:{node|formatnode}',
906 907 'file_copy': '{name} ({source})',
907 908 'extra': '{key}={value|stringescape}'
908 909 }
909 910 # filecopy is preserved for compatibility reasons
910 911 defaulttempl['filecopy'] = defaulttempl['file_copy']
911 912 self.t = templater.templater(mapfile, {'formatnode': formatnode},
912 913 cache=defaulttempl)
913 914 self.cache = {}
914 915
915 916 def use_template(self, t):
916 917 '''set template string to use'''
917 918 self.t.cache['changeset'] = t
918 919
919 920 def _meaningful_parentrevs(self, ctx):
920 921 """Return list of meaningful (or all if debug) parentrevs for rev.
921 922 """
922 923 parents = ctx.parents()
923 924 if len(parents) > 1:
924 925 return parents
925 926 if self.ui.debugflag:
926 927 return [parents[0], self.repo['null']]
927 928 if parents[0].rev() >= ctx.rev() - 1:
928 929 return []
929 930 return parents
930 931
931 932 def _show(self, ctx, copies, matchfn, props):
932 933 '''show a single changeset or file revision'''
933 934
934 935 showlist = templatekw.showlist
935 936
936 937 # showparents() behaviour depends on ui trace level which
937 938 # causes unexpected behaviours at templating level and makes
938 939 # it harder to extract it in a standalone function. Its
939 940 # behaviour cannot be changed so leave it here for now.
940 941 def showparents(**args):
941 942 ctx = args['ctx']
942 943 parents = [[('rev', p.rev()), ('node', p.hex())]
943 944 for p in self._meaningful_parentrevs(ctx)]
944 945 return showlist('parent', parents, **args)
945 946
946 947 props = props.copy()
947 948 props.update(templatekw.keywords)
948 949 props['parents'] = showparents
949 950 props['templ'] = self.t
950 951 props['ctx'] = ctx
951 952 props['repo'] = self.repo
952 953 props['revcache'] = {'copies': copies}
953 954 props['cache'] = self.cache
954 955
955 956 # find correct templates for current mode
956 957
957 958 tmplmodes = [
958 959 (True, None),
959 960 (self.ui.verbose, 'verbose'),
960 961 (self.ui.quiet, 'quiet'),
961 962 (self.ui.debugflag, 'debug'),
962 963 ]
963 964
964 965 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
965 966 for mode, postfix in tmplmodes:
966 967 for type in types:
967 968 cur = postfix and ('%s_%s' % (type, postfix)) or type
968 969 if mode and cur in self.t:
969 970 types[type] = cur
970 971
971 972 try:
972 973
973 974 # write header
974 975 if types['header']:
975 976 h = templater.stringify(self.t(types['header'], **props))
976 977 if self.buffered:
977 978 self.header[ctx.rev()] = h
978 979 else:
979 980 if self.lastheader != h:
980 981 self.lastheader = h
981 982 self.ui.write(h)
982 983
983 984 # write changeset metadata, then patch if requested
984 985 key = types['changeset']
985 986 self.ui.write(templater.stringify(self.t(key, **props)))
986 987 self.showpatch(ctx.node(), matchfn)
987 988
988 989 if types['footer']:
989 990 if not self.footer:
990 991 self.footer = templater.stringify(self.t(types['footer'],
991 992 **props))
992 993
993 994 except KeyError, inst:
994 995 msg = _("%s: no key named '%s'")
995 996 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
996 997 except SyntaxError, inst:
997 998 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
998 999
999 1000 def show_changeset(ui, repo, opts, buffered=False):
1000 1001 """show one changeset using template or regular display.
1001 1002
1002 1003 Display format will be the first non-empty hit of:
1003 1004 1. option 'template'
1004 1005 2. option 'style'
1005 1006 3. [ui] setting 'logtemplate'
1006 1007 4. [ui] setting 'style'
1007 1008 If all of these values are either the unset or the empty string,
1008 1009 regular display via changeset_printer() is done.
1009 1010 """
1010 1011 # options
1011 1012 patch = False
1012 1013 if opts.get('patch') or opts.get('stat'):
1013 1014 patch = matchall(repo)
1014 1015
1015 1016 tmpl = opts.get('template')
1016 1017 style = None
1017 1018 if tmpl:
1018 1019 tmpl = templater.parsestring(tmpl, quoted=False)
1019 1020 else:
1020 1021 style = opts.get('style')
1021 1022
1022 1023 # ui settings
1023 1024 if not (tmpl or style):
1024 1025 tmpl = ui.config('ui', 'logtemplate')
1025 1026 if tmpl:
1026 1027 tmpl = templater.parsestring(tmpl)
1027 1028 else:
1028 1029 style = util.expandpath(ui.config('ui', 'style', ''))
1029 1030
1030 1031 if not (tmpl or style):
1031 1032 return changeset_printer(ui, repo, patch, opts, buffered)
1032 1033
1033 1034 mapfile = None
1034 1035 if style and not tmpl:
1035 1036 mapfile = style
1036 1037 if not os.path.split(mapfile)[0]:
1037 1038 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1038 1039 or templater.templatepath(mapfile))
1039 1040 if mapname:
1040 1041 mapfile = mapname
1041 1042
1042 1043 try:
1043 1044 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
1044 1045 except SyntaxError, inst:
1045 1046 raise util.Abort(inst.args[0])
1046 1047 if tmpl:
1047 1048 t.use_template(tmpl)
1048 1049 return t
1049 1050
1050 1051 def finddate(ui, repo, date):
1051 1052 """Find the tipmost changeset that matches the given date spec"""
1052 1053
1053 1054 df = util.matchdate(date)
1054 1055 m = matchall(repo)
1055 1056 results = {}
1056 1057
1057 1058 def prep(ctx, fns):
1058 1059 d = ctx.date()
1059 1060 if df(d[0]):
1060 1061 results[ctx.rev()] = d
1061 1062
1062 1063 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1063 1064 rev = ctx.rev()
1064 1065 if rev in results:
1065 1066 ui.status(_("Found revision %s from %s\n") %
1066 1067 (rev, util.datestr(results[rev])))
1067 1068 return str(rev)
1068 1069
1069 1070 raise util.Abort(_("revision matching date not found"))
1070 1071
1071 1072 def walkchangerevs(repo, match, opts, prepare):
1072 1073 '''Iterate over files and the revs in which they changed.
1073 1074
1074 1075 Callers most commonly need to iterate backwards over the history
1075 1076 in which they are interested. Doing so has awful (quadratic-looking)
1076 1077 performance, so we use iterators in a "windowed" way.
1077 1078
1078 1079 We walk a window of revisions in the desired order. Within the
1079 1080 window, we first walk forwards to gather data, then in the desired
1080 1081 order (usually backwards) to display it.
1081 1082
1082 1083 This function returns an iterator yielding contexts. Before
1083 1084 yielding each context, the iterator will first call the prepare
1084 1085 function on each context in the window in forward order.'''
1085 1086
1086 1087 def increasing_windows(start, end, windowsize=8, sizelimit=512):
1087 1088 if start < end:
1088 1089 while start < end:
1089 1090 yield start, min(windowsize, end - start)
1090 1091 start += windowsize
1091 1092 if windowsize < sizelimit:
1092 1093 windowsize *= 2
1093 1094 else:
1094 1095 while start > end:
1095 1096 yield start, min(windowsize, start - end - 1)
1096 1097 start -= windowsize
1097 1098 if windowsize < sizelimit:
1098 1099 windowsize *= 2
1099 1100
1100 1101 follow = opts.get('follow') or opts.get('follow_first')
1101 1102
1102 1103 if not len(repo):
1103 1104 return []
1104 1105
1105 1106 if follow:
1106 1107 defrange = '%s:0' % repo['.'].rev()
1107 1108 else:
1108 1109 defrange = '-1:0'
1109 1110 revs = revrange(repo, opts['rev'] or [defrange])
1110 1111 if not revs:
1111 1112 return []
1112 1113 wanted = set()
1113 1114 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1114 1115 fncache = {}
1115 1116 change = util.cachefunc(repo.changectx)
1116 1117
1117 1118 # First step is to fill wanted, the set of revisions that we want to yield.
1118 1119 # When it does not induce extra cost, we also fill fncache for revisions in
1119 1120 # wanted: a cache of filenames that were changed (ctx.files()) and that
1120 1121 # match the file filtering conditions.
1121 1122
1122 1123 if not slowpath and not match.files():
1123 1124 # No files, no patterns. Display all revs.
1124 1125 wanted = set(revs)
1125 1126 copies = []
1126 1127
1127 1128 if not slowpath:
1128 1129 # We only have to read through the filelog to find wanted revisions
1129 1130
1130 1131 minrev, maxrev = min(revs), max(revs)
1131 1132 def filerevgen(filelog, last):
1132 1133 """
1133 1134 Only files, no patterns. Check the history of each file.
1134 1135
1135 1136 Examines filelog entries within minrev, maxrev linkrev range
1136 1137 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1137 1138 tuples in backwards order
1138 1139 """
1139 1140 cl_count = len(repo)
1140 1141 revs = []
1141 1142 for j in xrange(0, last + 1):
1142 1143 linkrev = filelog.linkrev(j)
1143 1144 if linkrev < minrev:
1144 1145 continue
1145 1146 # only yield rev for which we have the changelog, it can
1146 1147 # happen while doing "hg log" during a pull or commit
1147 1148 if linkrev >= cl_count:
1148 1149 break
1149 1150
1150 1151 parentlinkrevs = []
1151 1152 for p in filelog.parentrevs(j):
1152 1153 if p != nullrev:
1153 1154 parentlinkrevs.append(filelog.linkrev(p))
1154 1155 n = filelog.node(j)
1155 1156 revs.append((linkrev, parentlinkrevs,
1156 1157 follow and filelog.renamed(n)))
1157 1158
1158 1159 return reversed(revs)
1159 1160 def iterfiles():
1160 1161 for filename in match.files():
1161 1162 yield filename, None
1162 1163 for filename_node in copies:
1163 1164 yield filename_node
1164 1165 for file_, node in iterfiles():
1165 1166 filelog = repo.file(file_)
1166 1167 if not len(filelog):
1167 1168 if node is None:
1168 1169 # A zero count may be a directory or deleted file, so
1169 1170 # try to find matching entries on the slow path.
1170 1171 if follow:
1171 1172 raise util.Abort(
1172 1173 _('cannot follow nonexistent file: "%s"') % file_)
1173 1174 slowpath = True
1174 1175 break
1175 1176 else:
1176 1177 continue
1177 1178
1178 1179 if node is None:
1179 1180 last = len(filelog) - 1
1180 1181 else:
1181 1182 last = filelog.rev(node)
1182 1183
1183 1184
1184 1185 # keep track of all ancestors of the file
1185 1186 ancestors = set([filelog.linkrev(last)])
1186 1187
1187 1188 # iterate from latest to oldest revision
1188 1189 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1189 1190 if not follow:
1190 1191 if rev > maxrev:
1191 1192 continue
1192 1193 else:
1193 1194 # Note that last might not be the first interesting
1194 1195 # rev to us:
1195 1196 # if the file has been changed after maxrev, we'll
1196 1197 # have linkrev(last) > maxrev, and we still need
1197 1198 # to explore the file graph
1198 1199 if rev not in ancestors:
1199 1200 continue
1200 1201 # XXX insert 1327 fix here
1201 1202 if flparentlinkrevs:
1202 1203 ancestors.update(flparentlinkrevs)
1203 1204
1204 1205 fncache.setdefault(rev, []).append(file_)
1205 1206 wanted.add(rev)
1206 1207 if copied:
1207 1208 copies.append(copied)
1208 1209 if slowpath:
1209 1210 # We have to read the changelog to match filenames against
1210 1211 # changed files
1211 1212
1212 1213 if follow:
1213 1214 raise util.Abort(_('can only follow copies/renames for explicit '
1214 1215 'filenames'))
1215 1216
1216 1217 # The slow path checks files modified in every changeset.
1217 1218 for i in sorted(revs):
1218 1219 ctx = change(i)
1219 1220 matches = filter(match, ctx.files())
1220 1221 if matches:
1221 1222 fncache[i] = matches
1222 1223 wanted.add(i)
1223 1224
1224 1225 class followfilter(object):
1225 1226 def __init__(self, onlyfirst=False):
1226 1227 self.startrev = nullrev
1227 1228 self.roots = set()
1228 1229 self.onlyfirst = onlyfirst
1229 1230
1230 1231 def match(self, rev):
1231 1232 def realparents(rev):
1232 1233 if self.onlyfirst:
1233 1234 return repo.changelog.parentrevs(rev)[0:1]
1234 1235 else:
1235 1236 return filter(lambda x: x != nullrev,
1236 1237 repo.changelog.parentrevs(rev))
1237 1238
1238 1239 if self.startrev == nullrev:
1239 1240 self.startrev = rev
1240 1241 return True
1241 1242
1242 1243 if rev > self.startrev:
1243 1244 # forward: all descendants
1244 1245 if not self.roots:
1245 1246 self.roots.add(self.startrev)
1246 1247 for parent in realparents(rev):
1247 1248 if parent in self.roots:
1248 1249 self.roots.add(rev)
1249 1250 return True
1250 1251 else:
1251 1252 # backwards: all parents
1252 1253 if not self.roots:
1253 1254 self.roots.update(realparents(self.startrev))
1254 1255 if rev in self.roots:
1255 1256 self.roots.remove(rev)
1256 1257 self.roots.update(realparents(rev))
1257 1258 return True
1258 1259
1259 1260 return False
1260 1261
1261 1262 # it might be worthwhile to do this in the iterator if the rev range
1262 1263 # is descending and the prune args are all within that range
1263 1264 for rev in opts.get('prune', ()):
1264 1265 rev = repo.changelog.rev(repo.lookup(rev))
1265 1266 ff = followfilter()
1266 1267 stop = min(revs[0], revs[-1])
1267 1268 for x in xrange(rev, stop - 1, -1):
1268 1269 if ff.match(x):
1269 1270 wanted.discard(x)
1270 1271
1271 1272 # Now that wanted is correctly initialized, we can iterate over the
1272 1273 # revision range, yielding only revisions in wanted.
1273 1274 def iterate():
1274 1275 if follow and not match.files():
1275 1276 ff = followfilter(onlyfirst=opts.get('follow_first'))
1276 1277 def want(rev):
1277 1278 return ff.match(rev) and rev in wanted
1278 1279 else:
1279 1280 def want(rev):
1280 1281 return rev in wanted
1281 1282
1282 1283 for i, window in increasing_windows(0, len(revs)):
1283 1284 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1284 1285 for rev in sorted(nrevs):
1285 1286 fns = fncache.get(rev)
1286 1287 ctx = change(rev)
1287 1288 if not fns:
1288 1289 def fns_generator():
1289 1290 for f in ctx.files():
1290 1291 if match(f):
1291 1292 yield f
1292 1293 fns = fns_generator()
1293 1294 prepare(ctx, fns)
1294 1295 for rev in nrevs:
1295 1296 yield change(rev)
1296 1297 return iterate()
1297 1298
1298 1299 def add(ui, repo, match, dryrun, listsubrepos, prefix):
1299 1300 join = lambda f: os.path.join(prefix, f)
1300 1301 bad = []
1301 1302 oldbad = match.bad
1302 1303 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1303 1304 names = []
1304 1305 wctx = repo[None]
1305 1306 for f in repo.walk(match):
1306 1307 exact = match.exact(f)
1307 1308 if exact or f not in repo.dirstate:
1308 1309 names.append(f)
1309 1310 if ui.verbose or not exact:
1310 1311 ui.status(_('adding %s\n') % match.rel(join(f)))
1311 1312
1312 1313 if listsubrepos:
1313 1314 for subpath in wctx.substate:
1314 1315 sub = wctx.sub(subpath)
1315 1316 try:
1316 1317 submatch = matchmod.narrowmatcher(subpath, match)
1317 1318 bad.extend(sub.add(ui, submatch, dryrun, prefix))
1318 1319 except error.LookupError:
1319 1320 ui.status(_("skipping missing subrepository: %s\n")
1320 1321 % join(subpath))
1321 1322
1322 1323 if not dryrun:
1323 1324 rejected = wctx.add(names, prefix)
1324 1325 bad.extend(f for f in rejected if f in match.files())
1325 1326 return bad
1326 1327
1327 1328 def commit(ui, repo, commitfunc, pats, opts):
1328 1329 '''commit the specified files or all outstanding changes'''
1329 1330 date = opts.get('date')
1330 1331 if date:
1331 1332 opts['date'] = util.parsedate(date)
1332 1333 message = logmessage(opts)
1333 1334
1334 1335 # extract addremove carefully -- this function can be called from a command
1335 1336 # that doesn't support addremove
1336 1337 if opts.get('addremove'):
1337 1338 addremove(repo, pats, opts)
1338 1339
1339 1340 return commitfunc(ui, repo, message, match(repo, pats, opts), opts)
1340 1341
1341 1342 def commiteditor(repo, ctx, subs):
1342 1343 if ctx.description():
1343 1344 return ctx.description()
1344 1345 return commitforceeditor(repo, ctx, subs)
1345 1346
1346 1347 def commitforceeditor(repo, ctx, subs):
1347 1348 edittext = []
1348 1349 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1349 1350 if ctx.description():
1350 1351 edittext.append(ctx.description())
1351 1352 edittext.append("")
1352 1353 edittext.append("") # Empty line between message and comments.
1353 1354 edittext.append(_("HG: Enter commit message."
1354 1355 " Lines beginning with 'HG:' are removed."))
1355 1356 edittext.append(_("HG: Leave message empty to abort commit."))
1356 1357 edittext.append("HG: --")
1357 1358 edittext.append(_("HG: user: %s") % ctx.user())
1358 1359 if ctx.p2():
1359 1360 edittext.append(_("HG: branch merge"))
1360 1361 if ctx.branch():
1361 1362 edittext.append(_("HG: branch '%s'") % ctx.branch())
1362 1363 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1363 1364 edittext.extend([_("HG: added %s") % f for f in added])
1364 1365 edittext.extend([_("HG: changed %s") % f for f in modified])
1365 1366 edittext.extend([_("HG: removed %s") % f for f in removed])
1366 1367 if not added and not modified and not removed:
1367 1368 edittext.append(_("HG: no files changed"))
1368 1369 edittext.append("")
1369 1370 # run editor in the repository root
1370 1371 olddir = os.getcwd()
1371 1372 os.chdir(repo.root)
1372 1373 text = repo.ui.edit("\n".join(edittext), ctx.user())
1373 1374 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
1374 1375 os.chdir(olddir)
1375 1376
1376 1377 if not text.strip():
1377 1378 raise util.Abort(_("empty commit message"))
1378 1379
1379 1380 return text
@@ -1,483 +1,483 b''
1 1 $ cp "$TESTDIR"/printenv.py .
2 2
3 3 commit hooks can see env vars
4 4
5 5 $ hg init a
6 6 $ cd a
7 7 $ echo "[hooks]" > .hg/hgrc
8 8 $ echo 'commit = unset HG_LOCAL HG_TAG; python ../printenv.py commit' >> .hg/hgrc
9 9 $ echo 'commit.b = unset HG_LOCAL HG_TAG; python ../printenv.py commit.b' >> .hg/hgrc
10 10 $ echo 'precommit = unset HG_LOCAL HG_NODE HG_TAG; python ../printenv.py precommit' >> .hg/hgrc
11 11 $ echo 'pretxncommit = unset HG_LOCAL HG_TAG; python ../printenv.py pretxncommit' >> .hg/hgrc
12 12 $ echo 'pretxncommit.tip = hg -q tip' >> .hg/hgrc
13 13 $ echo 'pre-identify = python ../printenv.py pre-identify 1' >> .hg/hgrc
14 14 $ echo 'pre-cat = python ../printenv.py pre-cat' >> .hg/hgrc
15 15 $ echo 'post-cat = python ../printenv.py post-cat' >> .hg/hgrc
16 16 $ echo a > a
17 17 $ hg add a
18 18 $ hg commit -m a
19 19 precommit hook: HG_PARENT1=0000000000000000000000000000000000000000
20 20 pretxncommit hook: HG_NODE=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PARENT1=0000000000000000000000000000000000000000 HG_PENDING=$TESTTMP/a
21 21 0:cb9a9f314b8b
22 22 commit hook: HG_NODE=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PARENT1=0000000000000000000000000000000000000000
23 23 commit.b hook: HG_NODE=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PARENT1=0000000000000000000000000000000000000000
24 24
25 25 $ hg clone . ../b
26 26 updating to branch default
27 27 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
28 28 $ cd ../b
29 29
30 30 changegroup hooks can see env vars
31 31
32 32 $ echo '[hooks]' > .hg/hgrc
33 33 $ echo 'prechangegroup = python ../printenv.py prechangegroup' >> .hg/hgrc
34 34 $ echo 'changegroup = python ../printenv.py changegroup' >> .hg/hgrc
35 35 $ echo 'incoming = python ../printenv.py incoming' >> .hg/hgrc
36 36
37 37 pretxncommit and commit hooks can see both parents of merge
38 38
39 39 $ cd ../a
40 40 $ echo b >> a
41 41 $ hg commit -m a1 -d "1 0"
42 42 precommit hook: HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
43 43 pretxncommit hook: HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PENDING=$TESTTMP/a
44 44 1:ab228980c14d
45 45 commit hook: HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
46 46 commit.b hook: HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
47 47 $ hg update -C 0
48 48 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
49 49 $ echo b > b
50 50 $ hg add b
51 51 $ hg commit -m b -d '1 0'
52 52 precommit hook: HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
53 53 pretxncommit hook: HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PENDING=$TESTTMP/a
54 54 2:ee9deb46ab31
55 55 commit hook: HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
56 56 commit.b hook: HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
57 57 created new head
58 58 $ hg merge 1
59 59 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
60 60 (branch merge, don't forget to commit)
61 61 $ hg commit -m merge -d '2 0'
62 62 precommit hook: HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd
63 63 pretxncommit hook: HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd HG_PENDING=$TESTTMP/a
64 64 3:07f3376c1e65
65 65 commit hook: HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd
66 66 commit.b hook: HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd
67 67
68 68 test generic hooks
69 69
70 70 $ hg id
71 71 pre-identify hook: HG_ARGS=id HG_OPTS={'tags': None, 'rev': '', 'num': None, 'branch': None, 'id': None} HG_PATS=[]
72 72 warning: pre-identify hook exited with status 1
73 73 [1]
74 74 $ hg cat b
75 75 pre-cat hook: HG_ARGS=cat b HG_OPTS={'rev': '', 'decode': None, 'exclude': [], 'output': '', 'include': []} HG_PATS=['b']
76 b
76 77 post-cat hook: HG_ARGS=cat b HG_OPTS={'rev': '', 'decode': None, 'exclude': [], 'output': '', 'include': []} HG_PATS=['b'] HG_RESULT=0
77 b
78 78
79 79 $ cd ../b
80 80 $ hg pull ../a
81 81 prechangegroup hook: HG_SOURCE=pull HG_URL=file:
82 82 changegroup hook: HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_SOURCE=pull HG_URL=file:
83 83 incoming hook: HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_SOURCE=pull HG_URL=file:
84 84 incoming hook: HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_SOURCE=pull HG_URL=file:
85 85 incoming hook: HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_SOURCE=pull HG_URL=file:
86 86 pulling from ../a
87 87 searching for changes
88 88 adding changesets
89 89 adding manifests
90 90 adding file changes
91 91 added 3 changesets with 2 changes to 2 files
92 92 (run 'hg update' to get a working copy)
93 93
94 94 tag hooks can see env vars
95 95
96 96 $ cd ../a
97 97 $ echo 'pretag = python ../printenv.py pretag' >> .hg/hgrc
98 98 $ echo 'tag = unset HG_PARENT1 HG_PARENT2; python ../printenv.py tag' >> .hg/hgrc
99 99 $ hg tag -d '3 0' a
100 100 pretag hook: HG_LOCAL=0 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_TAG=a
101 101 precommit hook: HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2
102 102 pretxncommit hook: HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2 HG_PENDING=$TESTTMP/a
103 103 4:539e4b31b6dc
104 104 commit hook: HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2
105 105 commit.b hook: HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2
106 106 tag hook: HG_LOCAL=0 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_TAG=a
107 107 $ hg tag -l la
108 108 pretag hook: HG_LOCAL=1 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_TAG=la
109 109 tag hook: HG_LOCAL=1 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_TAG=la
110 110
111 111 pretag hook can forbid tagging
112 112
113 113 $ echo 'pretag.forbid = python ../printenv.py pretag.forbid 1' >> .hg/hgrc
114 114 $ hg tag -d '4 0' fa
115 115 pretag hook: HG_LOCAL=0 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_TAG=fa
116 116 pretag.forbid hook: HG_LOCAL=0 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_TAG=fa
117 117 abort: pretag.forbid hook exited with status 1
118 118 [255]
119 119 $ hg tag -l fla
120 120 pretag hook: HG_LOCAL=1 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_TAG=fla
121 121 pretag.forbid hook: HG_LOCAL=1 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_TAG=fla
122 122 abort: pretag.forbid hook exited with status 1
123 123 [255]
124 124
125 125 pretxncommit hook can see changeset, can roll back txn, changeset no
126 126 more there after
127 127
128 128 $ echo 'pretxncommit.forbid0 = hg tip -q' >> .hg/hgrc
129 129 $ echo 'pretxncommit.forbid1 = python ../printenv.py pretxncommit.forbid 1' >> .hg/hgrc
130 130 $ echo z > z
131 131 $ hg add z
132 132 $ hg -q tip
133 133 4:539e4b31b6dc
134 134 $ hg commit -m 'fail' -d '4 0'
135 135 precommit hook: HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
136 136 pretxncommit hook: HG_NODE=6f611f8018c10e827fee6bd2bc807f937e761567 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PENDING=$TESTTMP/a
137 137 5:6f611f8018c1
138 138 5:6f611f8018c1
139 139 pretxncommit.forbid hook: HG_NODE=6f611f8018c10e827fee6bd2bc807f937e761567 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PENDING=$TESTTMP/a
140 140 transaction abort!
141 141 rollback completed
142 142 abort: pretxncommit.forbid1 hook exited with status 1
143 143 [255]
144 144 $ hg -q tip
145 145 4:539e4b31b6dc
146 146
147 147 precommit hook can prevent commit
148 148
149 149 $ echo 'precommit.forbid = python ../printenv.py precommit.forbid 1' >> .hg/hgrc
150 150 $ hg commit -m 'fail' -d '4 0'
151 151 precommit hook: HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
152 152 precommit.forbid hook: HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
153 153 abort: precommit.forbid hook exited with status 1
154 154 [255]
155 155 $ hg -q tip
156 156 4:539e4b31b6dc
157 157
158 158 preupdate hook can prevent update
159 159
160 160 $ echo 'preupdate = python ../printenv.py preupdate' >> .hg/hgrc
161 161 $ hg update 1
162 162 preupdate hook: HG_PARENT1=ab228980c14d
163 163 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
164 164
165 165 update hook
166 166
167 167 $ echo 'update = python ../printenv.py update' >> .hg/hgrc
168 168 $ hg update
169 169 preupdate hook: HG_PARENT1=539e4b31b6dc
170 170 update hook: HG_ERROR=0 HG_PARENT1=539e4b31b6dc
171 171 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
172 172
173 173 prechangegroup hook can prevent incoming changes
174 174
175 175 $ cd ../b
176 176 $ hg -q tip
177 177 3:07f3376c1e65
178 178 $ echo '[hooks]' > .hg/hgrc
179 179 $ echo 'prechangegroup.forbid = python ../printenv.py prechangegroup.forbid 1' >> .hg/hgrc
180 180 $ hg pull ../a
181 181 prechangegroup.forbid hook: HG_SOURCE=pull HG_URL=file:
182 182 pulling from ../a
183 183 searching for changes
184 184 abort: prechangegroup.forbid hook exited with status 1
185 185 [255]
186 186
187 187 pretxnchangegroup hook can see incoming changes, can roll back txn,
188 188 incoming changes no longer there after
189 189
190 190 $ echo '[hooks]' > .hg/hgrc
191 191 $ echo 'pretxnchangegroup.forbid0 = hg tip -q' >> .hg/hgrc
192 192 $ echo 'pretxnchangegroup.forbid1 = python ../printenv.py pretxnchangegroup.forbid 1' >> .hg/hgrc
193 193 $ hg pull ../a
194 194 4:539e4b31b6dc
195 195 pretxnchangegroup.forbid hook: HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PENDING=$TESTTMP/b HG_SOURCE=pull HG_URL=file:
196 196 pulling from ../a
197 197 searching for changes
198 198 adding changesets
199 199 adding manifests
200 200 adding file changes
201 201 added 1 changesets with 1 changes to 1 files
202 202 transaction abort!
203 203 rollback completed
204 204 abort: pretxnchangegroup.forbid1 hook exited with status 1
205 205 [255]
206 206 $ hg -q tip
207 207 3:07f3376c1e65
208 208
209 209 outgoing hooks can see env vars
210 210
211 211 $ rm .hg/hgrc
212 212 $ echo '[hooks]' > ../a/.hg/hgrc
213 213 $ echo 'preoutgoing = python ../printenv.py preoutgoing' >> ../a/.hg/hgrc
214 214 $ echo 'outgoing = python ../printenv.py outgoing' >> ../a/.hg/hgrc
215 215 $ hg pull ../a
216 216 preoutgoing hook: HG_SOURCE=pull
217 217 outgoing hook: HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_SOURCE=pull
218 218 pulling from ../a
219 219 searching for changes
220 220 adding changesets
221 221 adding manifests
222 222 adding file changes
223 223 added 1 changesets with 1 changes to 1 files
224 224 (run 'hg update' to get a working copy)
225 225 $ hg rollback
226 226 rolling back to revision 3 (undo pull)
227 227
228 228 preoutgoing hook can prevent outgoing changes
229 229
230 230 $ echo 'preoutgoing.forbid = python ../printenv.py preoutgoing.forbid 1' >> ../a/.hg/hgrc
231 231 $ hg pull ../a
232 232 preoutgoing hook: HG_SOURCE=pull
233 233 preoutgoing.forbid hook: HG_SOURCE=pull
234 234 pulling from ../a
235 235 searching for changes
236 236 abort: preoutgoing.forbid hook exited with status 1
237 237 [255]
238 238
239 239 outgoing hooks work for local clones
240 240
241 241 $ cd ..
242 242 $ echo '[hooks]' > a/.hg/hgrc
243 243 $ echo 'preoutgoing = python ../printenv.py preoutgoing' >> a/.hg/hgrc
244 244 $ echo 'outgoing = python ../printenv.py outgoing' >> a/.hg/hgrc
245 245 $ hg clone a c
246 246 preoutgoing hook: HG_SOURCE=clone
247 247 outgoing hook: HG_NODE=0000000000000000000000000000000000000000 HG_SOURCE=clone
248 248 updating to branch default
249 249 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
250 250 $ rm -rf c
251 251
252 252 preoutgoing hook can prevent outgoing changes for local clones
253 253
254 254 $ echo 'preoutgoing.forbid = python ../printenv.py preoutgoing.forbid 1' >> a/.hg/hgrc
255 255 $ hg clone a zzz
256 256 preoutgoing hook: HG_SOURCE=clone
257 257 preoutgoing.forbid hook: HG_SOURCE=clone
258 258 abort: preoutgoing.forbid hook exited with status 1
259 259 [255]
260 260 $ cd b
261 261
262 262 $ cat > hooktests.py <<EOF
263 263 > from mercurial import util
264 264 >
265 265 > uncallable = 0
266 266 >
267 267 > def printargs(args):
268 268 > args.pop('ui', None)
269 269 > args.pop('repo', None)
270 270 > a = list(args.items())
271 271 > a.sort()
272 272 > print 'hook args:'
273 273 > for k, v in a:
274 274 > print ' ', k, v
275 275 >
276 276 > def passhook(**args):
277 277 > printargs(args)
278 278 >
279 279 > def failhook(**args):
280 280 > printargs(args)
281 281 > return True
282 282 >
283 283 > class LocalException(Exception):
284 284 > pass
285 285 >
286 286 > def raisehook(**args):
287 287 > raise LocalException('exception from hook')
288 288 >
289 289 > def aborthook(**args):
290 290 > raise util.Abort('raise abort from hook')
291 291 >
292 292 > def brokenhook(**args):
293 293 > return 1 + {}
294 294 >
295 295 > class container:
296 296 > unreachable = 1
297 297 > EOF
298 298
299 299 test python hooks
300 300
301 301 $ PYTHONPATH="`pwd`:$PYTHONPATH"
302 302 $ export PYTHONPATH
303 303
304 304 $ echo '[hooks]' > ../a/.hg/hgrc
305 305 $ echo 'preoutgoing.broken = python:hooktests.brokenhook' >> ../a/.hg/hgrc
306 306 $ hg pull ../a 2>&1 | grep 'raised an exception'
307 307 error: preoutgoing.broken hook raised an exception: unsupported operand type(s) for +: 'int' and 'dict'
308 308
309 309 $ echo '[hooks]' > ../a/.hg/hgrc
310 310 $ echo 'preoutgoing.raise = python:hooktests.raisehook' >> ../a/.hg/hgrc
311 311 $ hg pull ../a 2>&1 | grep 'raised an exception'
312 312 error: preoutgoing.raise hook raised an exception: exception from hook
313 313
314 314 $ echo '[hooks]' > ../a/.hg/hgrc
315 315 $ echo 'preoutgoing.abort = python:hooktests.aborthook' >> ../a/.hg/hgrc
316 316 $ hg pull ../a
317 317 pulling from ../a
318 318 searching for changes
319 319 error: preoutgoing.abort hook failed: raise abort from hook
320 320 abort: raise abort from hook
321 321 [255]
322 322
323 323 $ echo '[hooks]' > ../a/.hg/hgrc
324 324 $ echo 'preoutgoing.fail = python:hooktests.failhook' >> ../a/.hg/hgrc
325 325 $ hg pull ../a
326 326 pulling from ../a
327 327 searching for changes
328 328 hook args:
329 329 hooktype preoutgoing
330 330 source pull
331 331 abort: preoutgoing.fail hook failed
332 332 [255]
333 333
334 334 $ echo '[hooks]' > ../a/.hg/hgrc
335 335 $ echo 'preoutgoing.uncallable = python:hooktests.uncallable' >> ../a/.hg/hgrc
336 336 $ hg pull ../a
337 337 pulling from ../a
338 338 searching for changes
339 339 abort: preoutgoing.uncallable hook is invalid ("hooktests.uncallable" is not callable)
340 340 [255]
341 341
342 342 $ echo '[hooks]' > ../a/.hg/hgrc
343 343 $ echo 'preoutgoing.nohook = python:hooktests.nohook' >> ../a/.hg/hgrc
344 344 $ hg pull ../a
345 345 pulling from ../a
346 346 searching for changes
347 347 abort: preoutgoing.nohook hook is invalid ("hooktests.nohook" is not defined)
348 348 [255]
349 349
350 350 $ echo '[hooks]' > ../a/.hg/hgrc
351 351 $ echo 'preoutgoing.nomodule = python:nomodule' >> ../a/.hg/hgrc
352 352 $ hg pull ../a
353 353 pulling from ../a
354 354 searching for changes
355 355 abort: preoutgoing.nomodule hook is invalid ("nomodule" not in a module)
356 356 [255]
357 357
358 358 $ echo '[hooks]' > ../a/.hg/hgrc
359 359 $ echo 'preoutgoing.badmodule = python:nomodule.nowhere' >> ../a/.hg/hgrc
360 360 $ hg pull ../a
361 361 pulling from ../a
362 362 searching for changes
363 363 abort: preoutgoing.badmodule hook is invalid (import of "nomodule" failed)
364 364 [255]
365 365
366 366 $ echo '[hooks]' > ../a/.hg/hgrc
367 367 $ echo 'preoutgoing.unreachable = python:hooktests.container.unreachable' >> ../a/.hg/hgrc
368 368 $ hg pull ../a
369 369 pulling from ../a
370 370 searching for changes
371 371 abort: preoutgoing.unreachable hook is invalid (import of "hooktests.container" failed)
372 372 [255]
373 373
374 374 $ echo '[hooks]' > ../a/.hg/hgrc
375 375 $ echo 'preoutgoing.pass = python:hooktests.passhook' >> ../a/.hg/hgrc
376 376 $ hg pull ../a
377 377 pulling from ../a
378 378 searching for changes
379 379 hook args:
380 380 hooktype preoutgoing
381 381 source pull
382 382 adding changesets
383 383 adding manifests
384 384 adding file changes
385 385 added 1 changesets with 1 changes to 1 files
386 386 (run 'hg update' to get a working copy)
387 387
388 388 make sure --traceback works
389 389
390 390 $ echo '[hooks]' > .hg/hgrc
391 391 $ echo 'commit.abort = python:hooktests.aborthook' >> .hg/hgrc
392 392
393 393 $ echo aa > a
394 394 $ hg --traceback commit -d '0 0' -ma 2>&1 | grep '^Traceback'
395 395 Traceback (most recent call last):
396 396
397 397 $ cd ..
398 398 $ hg init c
399 399 $ cd c
400 400
401 401 $ cat > hookext.py <<EOF
402 402 > def autohook(**args):
403 403 > print "Automatically installed hook"
404 404 >
405 405 > def reposetup(ui, repo):
406 406 > repo.ui.setconfig("hooks", "commit.auto", autohook)
407 407 > EOF
408 408 $ echo '[extensions]' >> .hg/hgrc
409 409 $ echo 'hookext = hookext.py' >> .hg/hgrc
410 410
411 411 $ touch foo
412 412 $ hg add foo
413 413 $ hg ci -d '0 0' -m 'add foo'
414 414 Automatically installed hook
415 415 $ echo >> foo
416 416 $ hg ci --debug -d '0 0' -m 'change foo'
417 417 foo
418 418 calling hook commit.auto: <function autohook at *> (glob)
419 419 Automatically installed hook
420 420 committed changeset 1:52998019f6252a2b893452765fcb0a47351a5708
421 421
422 422 $ hg showconfig hooks
423 423 hooks.commit.auto=<function autohook at *> (glob)
424 424
425 425 test python hook configured with python:[file]:[hook] syntax
426 426
427 427 $ cd ..
428 428 $ mkdir d
429 429 $ cd d
430 430 $ hg init repo
431 431 $ mkdir hooks
432 432
433 433 $ cd hooks
434 434 $ cat > testhooks.py <<EOF
435 435 > def testhook(**args):
436 436 > print 'hook works'
437 437 > EOF
438 438 $ echo '[hooks]' > ../repo/.hg/hgrc
439 439 $ echo "pre-commit.test = python:`pwd`/testhooks.py:testhook" >> ../repo/.hg/hgrc
440 440
441 441 $ cd ../repo
442 442 $ hg commit -d '0 0'
443 443 hook works
444 444 nothing changed
445 445 [1]
446 446
447 447 $ cd ../../b
448 448
449 449 make sure --traceback works on hook import failure
450 450
451 451 $ cat > importfail.py <<EOF
452 452 > import somebogusmodule
453 453 > # dereference something in the module to force demandimport to load it
454 454 > somebogusmodule.whatever
455 455 > EOF
456 456
457 457 $ echo '[hooks]' > .hg/hgrc
458 458 $ echo 'precommit.importfail = python:importfail.whatever' >> .hg/hgrc
459 459
460 460 $ echo a >> a
461 461 $ hg --traceback commit -ma 2>&1 | egrep '^(exception|Traceback|ImportError)'
462 462 exception from first failed import attempt:
463 463 Traceback (most recent call last):
464 464 ImportError: No module named somebogusmodule
465 465 exception from second failed import attempt:
466 466 Traceback (most recent call last):
467 467 ImportError: No module named hgext_importfail
468 468 Traceback (most recent call last):
469 469
470 470 Issue1827: Hooks Update & Commit not completely post operation
471 471
472 472 commit and update hooks should run after command completion
473 473
474 474 $ echo '[hooks]' > .hg/hgrc
475 475 $ echo 'commit = hg id' >> .hg/hgrc
476 476 $ echo 'update = hg id' >> .hg/hgrc
477 477 $ echo bb > a
478 478 $ hg ci -ma
479 479 223eafe2750c tip
480 480 $ hg up 0
481 481 cb9a9f314b8b
482 482 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
483 483
General Comments 0
You need to be logged in to leave comments. Login now