##// END OF EJS Templates
cmdutil: fix mode handling in make_file
Adrian Buehlmann -
r13769:8796fb6a default
parent child Browse files
Show More
@@ -1,1389 +1,1389
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 writable = 'w' in mode or 'a' in mode
233 writable = mode not in ('r', 'rb')
234 234
235 235 if not pat or pat == '-':
236 236 fp = writable and sys.stdout or sys.stdin
237 237 return os.fdopen(os.dup(fp.fileno()), mode)
238 238 if hasattr(pat, 'write') and writable:
239 239 return pat
240 240 if hasattr(pat, 'read') and 'r' in mode:
241 241 return pat
242 242 return open(make_filename(repo, pat, node, total, seqno, revwidth,
243 243 pathname),
244 244 mode)
245 245
246 246 def expandpats(pats):
247 247 if not util.expandglobs:
248 248 return list(pats)
249 249 ret = []
250 250 for p in pats:
251 251 kind, name = matchmod._patsplit(p, None)
252 252 if kind is None:
253 253 try:
254 254 globbed = glob.glob(name)
255 255 except re.error:
256 256 globbed = [name]
257 257 if globbed:
258 258 ret.extend(globbed)
259 259 continue
260 260 ret.append(p)
261 261 return ret
262 262
263 263 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
264 264 if pats == ("",):
265 265 pats = []
266 266 if not globbed and default == 'relpath':
267 267 pats = expandpats(pats or [])
268 268 m = matchmod.match(repo.root, repo.getcwd(), pats,
269 269 opts.get('include'), opts.get('exclude'), default,
270 270 auditor=repo.auditor)
271 271 def badfn(f, msg):
272 272 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
273 273 m.bad = badfn
274 274 return m
275 275
276 276 def matchall(repo):
277 277 return matchmod.always(repo.root, repo.getcwd())
278 278
279 279 def matchfiles(repo, files):
280 280 return matchmod.exact(repo.root, repo.getcwd(), files)
281 281
282 282 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
283 283 if dry_run is None:
284 284 dry_run = opts.get('dry_run')
285 285 if similarity is None:
286 286 similarity = float(opts.get('similarity') or 0)
287 287 # we'd use status here, except handling of symlinks and ignore is tricky
288 288 added, unknown, deleted, removed = [], [], [], []
289 289 audit_path = util.path_auditor(repo.root)
290 290 m = match(repo, pats, opts)
291 291 for abs in repo.walk(m):
292 292 target = repo.wjoin(abs)
293 293 good = True
294 294 try:
295 295 audit_path(abs)
296 296 except:
297 297 good = False
298 298 rel = m.rel(abs)
299 299 exact = m.exact(abs)
300 300 if good and abs not in repo.dirstate:
301 301 unknown.append(abs)
302 302 if repo.ui.verbose or not exact:
303 303 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
304 304 elif repo.dirstate[abs] != 'r' and (not good or not os.path.lexists(target)
305 305 or (os.path.isdir(target) and not os.path.islink(target))):
306 306 deleted.append(abs)
307 307 if repo.ui.verbose or not exact:
308 308 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
309 309 # for finding renames
310 310 elif repo.dirstate[abs] == 'r':
311 311 removed.append(abs)
312 312 elif repo.dirstate[abs] == 'a':
313 313 added.append(abs)
314 314 copies = {}
315 315 if similarity > 0:
316 316 for old, new, score in similar.findrenames(repo,
317 317 added + unknown, removed + deleted, similarity):
318 318 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
319 319 repo.ui.status(_('recording removal of %s as rename to %s '
320 320 '(%d%% similar)\n') %
321 321 (m.rel(old), m.rel(new), score * 100))
322 322 copies[new] = old
323 323
324 324 if not dry_run:
325 325 wctx = repo[None]
326 326 wlock = repo.wlock()
327 327 try:
328 328 wctx.remove(deleted)
329 329 wctx.add(unknown)
330 330 for new, old in copies.iteritems():
331 331 wctx.copy(old, new)
332 332 finally:
333 333 wlock.release()
334 334
335 335 def updatedir(ui, repo, patches, similarity=0):
336 336 '''Update dirstate after patch application according to metadata'''
337 337 if not patches:
338 338 return
339 339 copies = []
340 340 removes = set()
341 341 cfiles = patches.keys()
342 342 cwd = repo.getcwd()
343 343 if cwd:
344 344 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
345 345 for f in patches:
346 346 gp = patches[f]
347 347 if not gp:
348 348 continue
349 349 if gp.op == 'RENAME':
350 350 copies.append((gp.oldpath, gp.path))
351 351 removes.add(gp.oldpath)
352 352 elif gp.op == 'COPY':
353 353 copies.append((gp.oldpath, gp.path))
354 354 elif gp.op == 'DELETE':
355 355 removes.add(gp.path)
356 356
357 357 wctx = repo[None]
358 358 for src, dst in copies:
359 359 dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd)
360 360 if (not similarity) and removes:
361 361 wctx.remove(sorted(removes), True)
362 362
363 363 for f in patches:
364 364 gp = patches[f]
365 365 if gp and gp.mode:
366 366 islink, isexec = gp.mode
367 367 dst = repo.wjoin(gp.path)
368 368 # patch won't create empty files
369 369 if gp.op == 'ADD' and not os.path.lexists(dst):
370 370 flags = (isexec and 'x' or '') + (islink and 'l' or '')
371 371 repo.wwrite(gp.path, '', flags)
372 372 util.set_flags(dst, islink, isexec)
373 373 addremove(repo, cfiles, similarity=similarity)
374 374 files = patches.keys()
375 375 files.extend([r for r in removes if r not in files])
376 376 return sorted(files)
377 377
378 378 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
379 379 """Update the dirstate to reflect the intent of copying src to dst. For
380 380 different reasons it might not end with dst being marked as copied from src.
381 381 """
382 382 origsrc = repo.dirstate.copied(src) or src
383 383 if dst == origsrc: # copying back a copy?
384 384 if repo.dirstate[dst] not in 'mn' and not dryrun:
385 385 repo.dirstate.normallookup(dst)
386 386 else:
387 387 if repo.dirstate[origsrc] == 'a' and origsrc == src:
388 388 if not ui.quiet:
389 389 ui.warn(_("%s has not been committed yet, so no copy "
390 390 "data will be stored for %s.\n")
391 391 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
392 392 if repo.dirstate[dst] in '?r' and not dryrun:
393 393 wctx.add([dst])
394 394 elif not dryrun:
395 395 wctx.copy(origsrc, dst)
396 396
397 397 def copy(ui, repo, pats, opts, rename=False):
398 398 # called with the repo lock held
399 399 #
400 400 # hgsep => pathname that uses "/" to separate directories
401 401 # ossep => pathname that uses os.sep to separate directories
402 402 cwd = repo.getcwd()
403 403 targets = {}
404 404 after = opts.get("after")
405 405 dryrun = opts.get("dry_run")
406 406 wctx = repo[None]
407 407
408 408 def walkpat(pat):
409 409 srcs = []
410 410 badstates = after and '?' or '?r'
411 411 m = match(repo, [pat], opts, globbed=True)
412 412 for abs in repo.walk(m):
413 413 state = repo.dirstate[abs]
414 414 rel = m.rel(abs)
415 415 exact = m.exact(abs)
416 416 if state in badstates:
417 417 if exact and state == '?':
418 418 ui.warn(_('%s: not copying - file is not managed\n') % rel)
419 419 if exact and state == 'r':
420 420 ui.warn(_('%s: not copying - file has been marked for'
421 421 ' remove\n') % rel)
422 422 continue
423 423 # abs: hgsep
424 424 # rel: ossep
425 425 srcs.append((abs, rel, exact))
426 426 return srcs
427 427
428 428 # abssrc: hgsep
429 429 # relsrc: ossep
430 430 # otarget: ossep
431 431 def copyfile(abssrc, relsrc, otarget, exact):
432 432 abstarget = util.canonpath(repo.root, cwd, otarget)
433 433 reltarget = repo.pathto(abstarget, cwd)
434 434 target = repo.wjoin(abstarget)
435 435 src = repo.wjoin(abssrc)
436 436 state = repo.dirstate[abstarget]
437 437
438 438 # check for collisions
439 439 prevsrc = targets.get(abstarget)
440 440 if prevsrc is not None:
441 441 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
442 442 (reltarget, repo.pathto(abssrc, cwd),
443 443 repo.pathto(prevsrc, cwd)))
444 444 return
445 445
446 446 # check for overwrites
447 447 exists = os.path.lexists(target)
448 448 if not after and exists or after and state in 'mn':
449 449 if not opts['force']:
450 450 ui.warn(_('%s: not overwriting - file exists\n') %
451 451 reltarget)
452 452 return
453 453
454 454 if after:
455 455 if not exists:
456 456 if rename:
457 457 ui.warn(_('%s: not recording move - %s does not exist\n') %
458 458 (relsrc, reltarget))
459 459 else:
460 460 ui.warn(_('%s: not recording copy - %s does not exist\n') %
461 461 (relsrc, reltarget))
462 462 return
463 463 elif not dryrun:
464 464 try:
465 465 if exists:
466 466 os.unlink(target)
467 467 targetdir = os.path.dirname(target) or '.'
468 468 if not os.path.isdir(targetdir):
469 469 os.makedirs(targetdir)
470 470 util.copyfile(src, target)
471 471 except IOError, inst:
472 472 if inst.errno == errno.ENOENT:
473 473 ui.warn(_('%s: deleted in working copy\n') % relsrc)
474 474 else:
475 475 ui.warn(_('%s: cannot copy - %s\n') %
476 476 (relsrc, inst.strerror))
477 477 return True # report a failure
478 478
479 479 if ui.verbose or not exact:
480 480 if rename:
481 481 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
482 482 else:
483 483 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
484 484
485 485 targets[abstarget] = abssrc
486 486
487 487 # fix up dirstate
488 488 dirstatecopy(ui, repo, wctx, abssrc, abstarget, dryrun=dryrun, cwd=cwd)
489 489 if rename and not dryrun:
490 490 wctx.remove([abssrc], not after)
491 491
492 492 # pat: ossep
493 493 # dest ossep
494 494 # srcs: list of (hgsep, hgsep, ossep, bool)
495 495 # return: function that takes hgsep and returns ossep
496 496 def targetpathfn(pat, dest, srcs):
497 497 if os.path.isdir(pat):
498 498 abspfx = util.canonpath(repo.root, cwd, pat)
499 499 abspfx = util.localpath(abspfx)
500 500 if destdirexists:
501 501 striplen = len(os.path.split(abspfx)[0])
502 502 else:
503 503 striplen = len(abspfx)
504 504 if striplen:
505 505 striplen += len(os.sep)
506 506 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
507 507 elif destdirexists:
508 508 res = lambda p: os.path.join(dest,
509 509 os.path.basename(util.localpath(p)))
510 510 else:
511 511 res = lambda p: dest
512 512 return res
513 513
514 514 # pat: ossep
515 515 # dest ossep
516 516 # srcs: list of (hgsep, hgsep, ossep, bool)
517 517 # return: function that takes hgsep and returns ossep
518 518 def targetpathafterfn(pat, dest, srcs):
519 519 if matchmod.patkind(pat):
520 520 # a mercurial pattern
521 521 res = lambda p: os.path.join(dest,
522 522 os.path.basename(util.localpath(p)))
523 523 else:
524 524 abspfx = util.canonpath(repo.root, cwd, pat)
525 525 if len(abspfx) < len(srcs[0][0]):
526 526 # A directory. Either the target path contains the last
527 527 # component of the source path or it does not.
528 528 def evalpath(striplen):
529 529 score = 0
530 530 for s in srcs:
531 531 t = os.path.join(dest, util.localpath(s[0])[striplen:])
532 532 if os.path.lexists(t):
533 533 score += 1
534 534 return score
535 535
536 536 abspfx = util.localpath(abspfx)
537 537 striplen = len(abspfx)
538 538 if striplen:
539 539 striplen += len(os.sep)
540 540 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
541 541 score = evalpath(striplen)
542 542 striplen1 = len(os.path.split(abspfx)[0])
543 543 if striplen1:
544 544 striplen1 += len(os.sep)
545 545 if evalpath(striplen1) > score:
546 546 striplen = striplen1
547 547 res = lambda p: os.path.join(dest,
548 548 util.localpath(p)[striplen:])
549 549 else:
550 550 # a file
551 551 if destdirexists:
552 552 res = lambda p: os.path.join(dest,
553 553 os.path.basename(util.localpath(p)))
554 554 else:
555 555 res = lambda p: dest
556 556 return res
557 557
558 558
559 559 pats = expandpats(pats)
560 560 if not pats:
561 561 raise util.Abort(_('no source or destination specified'))
562 562 if len(pats) == 1:
563 563 raise util.Abort(_('no destination specified'))
564 564 dest = pats.pop()
565 565 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
566 566 if not destdirexists:
567 567 if len(pats) > 1 or matchmod.patkind(pats[0]):
568 568 raise util.Abort(_('with multiple sources, destination must be an '
569 569 'existing directory'))
570 570 if util.endswithsep(dest):
571 571 raise util.Abort(_('destination %s is not a directory') % dest)
572 572
573 573 tfn = targetpathfn
574 574 if after:
575 575 tfn = targetpathafterfn
576 576 copylist = []
577 577 for pat in pats:
578 578 srcs = walkpat(pat)
579 579 if not srcs:
580 580 continue
581 581 copylist.append((tfn(pat, dest, srcs), srcs))
582 582 if not copylist:
583 583 raise util.Abort(_('no files to copy'))
584 584
585 585 errors = 0
586 586 for targetpath, srcs in copylist:
587 587 for abssrc, relsrc, exact in srcs:
588 588 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
589 589 errors += 1
590 590
591 591 if errors:
592 592 ui.warn(_('(consider using --after)\n'))
593 593
594 594 return errors != 0
595 595
596 596 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
597 597 runargs=None, appendpid=False):
598 598 '''Run a command as a service.'''
599 599
600 600 if opts['daemon'] and not opts['daemon_pipefds']:
601 601 # Signal child process startup with file removal
602 602 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
603 603 os.close(lockfd)
604 604 try:
605 605 if not runargs:
606 606 runargs = util.hgcmd() + sys.argv[1:]
607 607 runargs.append('--daemon-pipefds=%s' % lockpath)
608 608 # Don't pass --cwd to the child process, because we've already
609 609 # changed directory.
610 610 for i in xrange(1, len(runargs)):
611 611 if runargs[i].startswith('--cwd='):
612 612 del runargs[i]
613 613 break
614 614 elif runargs[i].startswith('--cwd'):
615 615 del runargs[i:i + 2]
616 616 break
617 617 def condfn():
618 618 return not os.path.exists(lockpath)
619 619 pid = util.rundetached(runargs, condfn)
620 620 if pid < 0:
621 621 raise util.Abort(_('child process failed to start'))
622 622 finally:
623 623 try:
624 624 os.unlink(lockpath)
625 625 except OSError, e:
626 626 if e.errno != errno.ENOENT:
627 627 raise
628 628 if parentfn:
629 629 return parentfn(pid)
630 630 else:
631 631 return
632 632
633 633 if initfn:
634 634 initfn()
635 635
636 636 if opts['pid_file']:
637 637 mode = appendpid and 'a' or 'w'
638 638 fp = open(opts['pid_file'], mode)
639 639 fp.write(str(os.getpid()) + '\n')
640 640 fp.close()
641 641
642 642 if opts['daemon_pipefds']:
643 643 lockpath = opts['daemon_pipefds']
644 644 try:
645 645 os.setsid()
646 646 except AttributeError:
647 647 pass
648 648 os.unlink(lockpath)
649 649 util.hidewindow()
650 650 sys.stdout.flush()
651 651 sys.stderr.flush()
652 652
653 653 nullfd = os.open(util.nulldev, os.O_RDWR)
654 654 logfilefd = nullfd
655 655 if logfile:
656 656 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
657 657 os.dup2(nullfd, 0)
658 658 os.dup2(logfilefd, 1)
659 659 os.dup2(logfilefd, 2)
660 660 if nullfd not in (0, 1, 2):
661 661 os.close(nullfd)
662 662 if logfile and logfilefd not in (0, 1, 2):
663 663 os.close(logfilefd)
664 664
665 665 if runfn:
666 666 return runfn()
667 667
668 668 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
669 669 opts=None):
670 670 '''export changesets as hg patches.'''
671 671
672 672 total = len(revs)
673 673 revwidth = max([len(str(rev)) for rev in revs])
674 674
675 675 def single(rev, seqno, fp):
676 676 ctx = repo[rev]
677 677 node = ctx.node()
678 678 parents = [p.node() for p in ctx.parents() if p]
679 679 branch = ctx.branch()
680 680 if switch_parent:
681 681 parents.reverse()
682 682 prev = (parents and parents[0]) or nullid
683 683
684 684 shouldclose = False
685 685 if not fp:
686 686 fp = make_file(repo, template, node, total=total, seqno=seqno,
687 687 revwidth=revwidth, mode='ab')
688 688 if fp != template:
689 689 shouldclose = True
690 690 if fp != sys.stdout and hasattr(fp, 'name'):
691 691 repo.ui.note("%s\n" % fp.name)
692 692
693 693 fp.write("# HG changeset patch\n")
694 694 fp.write("# User %s\n" % ctx.user())
695 695 fp.write("# Date %d %d\n" % ctx.date())
696 696 if branch and branch != 'default':
697 697 fp.write("# Branch %s\n" % branch)
698 698 fp.write("# Node ID %s\n" % hex(node))
699 699 fp.write("# Parent %s\n" % hex(prev))
700 700 if len(parents) > 1:
701 701 fp.write("# Parent %s\n" % hex(parents[1]))
702 702 fp.write(ctx.description().rstrip())
703 703 fp.write("\n\n")
704 704
705 705 for chunk in patch.diff(repo, prev, node, opts=opts):
706 706 fp.write(chunk)
707 707
708 708 if shouldclose:
709 709 fp.close()
710 710
711 711 for seqno, rev in enumerate(revs):
712 712 single(rev, seqno + 1, fp)
713 713
714 714 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
715 715 changes=None, stat=False, fp=None, prefix='',
716 716 listsubrepos=False):
717 717 '''show diff or diffstat.'''
718 718 if fp is None:
719 719 write = ui.write
720 720 else:
721 721 def write(s, **kw):
722 722 fp.write(s)
723 723
724 724 if stat:
725 725 diffopts = diffopts.copy(context=0)
726 726 width = 80
727 727 if not ui.plain():
728 728 width = ui.termwidth()
729 729 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
730 730 prefix=prefix)
731 731 for chunk, label in patch.diffstatui(util.iterlines(chunks),
732 732 width=width,
733 733 git=diffopts.git):
734 734 write(chunk, label=label)
735 735 else:
736 736 for chunk, label in patch.diffui(repo, node1, node2, match,
737 737 changes, diffopts, prefix=prefix):
738 738 write(chunk, label=label)
739 739
740 740 if listsubrepos:
741 741 ctx1 = repo[node1]
742 742 ctx2 = repo[node2]
743 743 for subpath, sub in subrepo.itersubrepos(ctx1, ctx2):
744 744 if node2 is not None:
745 745 node2 = ctx2.substate[subpath][1]
746 746 submatch = matchmod.narrowmatcher(subpath, match)
747 747 sub.diff(diffopts, node2, submatch, changes=changes,
748 748 stat=stat, fp=fp, prefix=prefix)
749 749
750 750 class changeset_printer(object):
751 751 '''show changeset information when templating not requested.'''
752 752
753 753 def __init__(self, ui, repo, patch, diffopts, buffered):
754 754 self.ui = ui
755 755 self.repo = repo
756 756 self.buffered = buffered
757 757 self.patch = patch
758 758 self.diffopts = diffopts
759 759 self.header = {}
760 760 self.hunk = {}
761 761 self.lastheader = None
762 762 self.footer = None
763 763
764 764 def flush(self, rev):
765 765 if rev in self.header:
766 766 h = self.header[rev]
767 767 if h != self.lastheader:
768 768 self.lastheader = h
769 769 self.ui.write(h)
770 770 del self.header[rev]
771 771 if rev in self.hunk:
772 772 self.ui.write(self.hunk[rev])
773 773 del self.hunk[rev]
774 774 return 1
775 775 return 0
776 776
777 777 def close(self):
778 778 if self.footer:
779 779 self.ui.write(self.footer)
780 780
781 781 def show(self, ctx, copies=None, matchfn=None, **props):
782 782 if self.buffered:
783 783 self.ui.pushbuffer()
784 784 self._show(ctx, copies, matchfn, props)
785 785 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
786 786 else:
787 787 self._show(ctx, copies, matchfn, props)
788 788
789 789 def _show(self, ctx, copies, matchfn, props):
790 790 '''show a single changeset or file revision'''
791 791 changenode = ctx.node()
792 792 rev = ctx.rev()
793 793
794 794 if self.ui.quiet:
795 795 self.ui.write("%d:%s\n" % (rev, short(changenode)),
796 796 label='log.node')
797 797 return
798 798
799 799 log = self.repo.changelog
800 800 date = util.datestr(ctx.date())
801 801
802 802 hexfunc = self.ui.debugflag and hex or short
803 803
804 804 parents = [(p, hexfunc(log.node(p)))
805 805 for p in self._meaningful_parentrevs(log, rev)]
806 806
807 807 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
808 808 label='log.changeset')
809 809
810 810 branch = ctx.branch()
811 811 # don't show the default branch name
812 812 if branch != 'default':
813 813 self.ui.write(_("branch: %s\n") % branch,
814 814 label='log.branch')
815 815 for bookmark in self.repo.nodebookmarks(changenode):
816 816 self.ui.write(_("bookmark: %s\n") % bookmark,
817 817 label='log.bookmark')
818 818 for tag in self.repo.nodetags(changenode):
819 819 self.ui.write(_("tag: %s\n") % tag,
820 820 label='log.tag')
821 821 for parent in parents:
822 822 self.ui.write(_("parent: %d:%s\n") % parent,
823 823 label='log.parent')
824 824
825 825 if self.ui.debugflag:
826 826 mnode = ctx.manifestnode()
827 827 self.ui.write(_("manifest: %d:%s\n") %
828 828 (self.repo.manifest.rev(mnode), hex(mnode)),
829 829 label='ui.debug log.manifest')
830 830 self.ui.write(_("user: %s\n") % ctx.user(),
831 831 label='log.user')
832 832 self.ui.write(_("date: %s\n") % date,
833 833 label='log.date')
834 834
835 835 if self.ui.debugflag:
836 836 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
837 837 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
838 838 files):
839 839 if value:
840 840 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
841 841 label='ui.debug log.files')
842 842 elif ctx.files() and self.ui.verbose:
843 843 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
844 844 label='ui.note log.files')
845 845 if copies and self.ui.verbose:
846 846 copies = ['%s (%s)' % c for c in copies]
847 847 self.ui.write(_("copies: %s\n") % ' '.join(copies),
848 848 label='ui.note log.copies')
849 849
850 850 extra = ctx.extra()
851 851 if extra and self.ui.debugflag:
852 852 for key, value in sorted(extra.items()):
853 853 self.ui.write(_("extra: %s=%s\n")
854 854 % (key, value.encode('string_escape')),
855 855 label='ui.debug log.extra')
856 856
857 857 description = ctx.description().strip()
858 858 if description:
859 859 if self.ui.verbose:
860 860 self.ui.write(_("description:\n"),
861 861 label='ui.note log.description')
862 862 self.ui.write(description,
863 863 label='ui.note log.description')
864 864 self.ui.write("\n\n")
865 865 else:
866 866 self.ui.write(_("summary: %s\n") %
867 867 description.splitlines()[0],
868 868 label='log.summary')
869 869 self.ui.write("\n")
870 870
871 871 self.showpatch(changenode, matchfn)
872 872
873 873 def showpatch(self, node, matchfn):
874 874 if not matchfn:
875 875 matchfn = self.patch
876 876 if matchfn:
877 877 stat = self.diffopts.get('stat')
878 878 diff = self.diffopts.get('patch')
879 879 diffopts = patch.diffopts(self.ui, self.diffopts)
880 880 prev = self.repo.changelog.parents(node)[0]
881 881 if stat:
882 882 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
883 883 match=matchfn, stat=True)
884 884 if diff:
885 885 if stat:
886 886 self.ui.write("\n")
887 887 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
888 888 match=matchfn, stat=False)
889 889 self.ui.write("\n")
890 890
891 891 def _meaningful_parentrevs(self, log, rev):
892 892 """Return list of meaningful (or all if debug) parentrevs for rev.
893 893
894 894 For merges (two non-nullrev revisions) both parents are meaningful.
895 895 Otherwise the first parent revision is considered meaningful if it
896 896 is not the preceding revision.
897 897 """
898 898 parents = log.parentrevs(rev)
899 899 if not self.ui.debugflag and parents[1] == nullrev:
900 900 if parents[0] >= rev - 1:
901 901 parents = []
902 902 else:
903 903 parents = [parents[0]]
904 904 return parents
905 905
906 906
907 907 class changeset_templater(changeset_printer):
908 908 '''format changeset information.'''
909 909
910 910 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
911 911 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
912 912 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
913 913 defaulttempl = {
914 914 'parent': '{rev}:{node|formatnode} ',
915 915 'manifest': '{rev}:{node|formatnode}',
916 916 'file_copy': '{name} ({source})',
917 917 'extra': '{key}={value|stringescape}'
918 918 }
919 919 # filecopy is preserved for compatibility reasons
920 920 defaulttempl['filecopy'] = defaulttempl['file_copy']
921 921 self.t = templater.templater(mapfile, {'formatnode': formatnode},
922 922 cache=defaulttempl)
923 923 self.cache = {}
924 924
925 925 def use_template(self, t):
926 926 '''set template string to use'''
927 927 self.t.cache['changeset'] = t
928 928
929 929 def _meaningful_parentrevs(self, ctx):
930 930 """Return list of meaningful (or all if debug) parentrevs for rev.
931 931 """
932 932 parents = ctx.parents()
933 933 if len(parents) > 1:
934 934 return parents
935 935 if self.ui.debugflag:
936 936 return [parents[0], self.repo['null']]
937 937 if parents[0].rev() >= ctx.rev() - 1:
938 938 return []
939 939 return parents
940 940
941 941 def _show(self, ctx, copies, matchfn, props):
942 942 '''show a single changeset or file revision'''
943 943
944 944 showlist = templatekw.showlist
945 945
946 946 # showparents() behaviour depends on ui trace level which
947 947 # causes unexpected behaviours at templating level and makes
948 948 # it harder to extract it in a standalone function. Its
949 949 # behaviour cannot be changed so leave it here for now.
950 950 def showparents(**args):
951 951 ctx = args['ctx']
952 952 parents = [[('rev', p.rev()), ('node', p.hex())]
953 953 for p in self._meaningful_parentrevs(ctx)]
954 954 return showlist('parent', parents, **args)
955 955
956 956 props = props.copy()
957 957 props.update(templatekw.keywords)
958 958 props['parents'] = showparents
959 959 props['templ'] = self.t
960 960 props['ctx'] = ctx
961 961 props['repo'] = self.repo
962 962 props['revcache'] = {'copies': copies}
963 963 props['cache'] = self.cache
964 964
965 965 # find correct templates for current mode
966 966
967 967 tmplmodes = [
968 968 (True, None),
969 969 (self.ui.verbose, 'verbose'),
970 970 (self.ui.quiet, 'quiet'),
971 971 (self.ui.debugflag, 'debug'),
972 972 ]
973 973
974 974 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
975 975 for mode, postfix in tmplmodes:
976 976 for type in types:
977 977 cur = postfix and ('%s_%s' % (type, postfix)) or type
978 978 if mode and cur in self.t:
979 979 types[type] = cur
980 980
981 981 try:
982 982
983 983 # write header
984 984 if types['header']:
985 985 h = templater.stringify(self.t(types['header'], **props))
986 986 if self.buffered:
987 987 self.header[ctx.rev()] = h
988 988 else:
989 989 if self.lastheader != h:
990 990 self.lastheader = h
991 991 self.ui.write(h)
992 992
993 993 # write changeset metadata, then patch if requested
994 994 key = types['changeset']
995 995 self.ui.write(templater.stringify(self.t(key, **props)))
996 996 self.showpatch(ctx.node(), matchfn)
997 997
998 998 if types['footer']:
999 999 if not self.footer:
1000 1000 self.footer = templater.stringify(self.t(types['footer'],
1001 1001 **props))
1002 1002
1003 1003 except KeyError, inst:
1004 1004 msg = _("%s: no key named '%s'")
1005 1005 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
1006 1006 except SyntaxError, inst:
1007 1007 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
1008 1008
1009 1009 def show_changeset(ui, repo, opts, buffered=False):
1010 1010 """show one changeset using template or regular display.
1011 1011
1012 1012 Display format will be the first non-empty hit of:
1013 1013 1. option 'template'
1014 1014 2. option 'style'
1015 1015 3. [ui] setting 'logtemplate'
1016 1016 4. [ui] setting 'style'
1017 1017 If all of these values are either the unset or the empty string,
1018 1018 regular display via changeset_printer() is done.
1019 1019 """
1020 1020 # options
1021 1021 patch = False
1022 1022 if opts.get('patch') or opts.get('stat'):
1023 1023 patch = matchall(repo)
1024 1024
1025 1025 tmpl = opts.get('template')
1026 1026 style = None
1027 1027 if tmpl:
1028 1028 tmpl = templater.parsestring(tmpl, quoted=False)
1029 1029 else:
1030 1030 style = opts.get('style')
1031 1031
1032 1032 # ui settings
1033 1033 if not (tmpl or style):
1034 1034 tmpl = ui.config('ui', 'logtemplate')
1035 1035 if tmpl:
1036 1036 tmpl = templater.parsestring(tmpl)
1037 1037 else:
1038 1038 style = util.expandpath(ui.config('ui', 'style', ''))
1039 1039
1040 1040 if not (tmpl or style):
1041 1041 return changeset_printer(ui, repo, patch, opts, buffered)
1042 1042
1043 1043 mapfile = None
1044 1044 if style and not tmpl:
1045 1045 mapfile = style
1046 1046 if not os.path.split(mapfile)[0]:
1047 1047 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1048 1048 or templater.templatepath(mapfile))
1049 1049 if mapname:
1050 1050 mapfile = mapname
1051 1051
1052 1052 try:
1053 1053 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
1054 1054 except SyntaxError, inst:
1055 1055 raise util.Abort(inst.args[0])
1056 1056 if tmpl:
1057 1057 t.use_template(tmpl)
1058 1058 return t
1059 1059
1060 1060 def finddate(ui, repo, date):
1061 1061 """Find the tipmost changeset that matches the given date spec"""
1062 1062
1063 1063 df = util.matchdate(date)
1064 1064 m = matchall(repo)
1065 1065 results = {}
1066 1066
1067 1067 def prep(ctx, fns):
1068 1068 d = ctx.date()
1069 1069 if df(d[0]):
1070 1070 results[ctx.rev()] = d
1071 1071
1072 1072 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1073 1073 rev = ctx.rev()
1074 1074 if rev in results:
1075 1075 ui.status(_("Found revision %s from %s\n") %
1076 1076 (rev, util.datestr(results[rev])))
1077 1077 return str(rev)
1078 1078
1079 1079 raise util.Abort(_("revision matching date not found"))
1080 1080
1081 1081 def walkchangerevs(repo, match, opts, prepare):
1082 1082 '''Iterate over files and the revs in which they changed.
1083 1083
1084 1084 Callers most commonly need to iterate backwards over the history
1085 1085 in which they are interested. Doing so has awful (quadratic-looking)
1086 1086 performance, so we use iterators in a "windowed" way.
1087 1087
1088 1088 We walk a window of revisions in the desired order. Within the
1089 1089 window, we first walk forwards to gather data, then in the desired
1090 1090 order (usually backwards) to display it.
1091 1091
1092 1092 This function returns an iterator yielding contexts. Before
1093 1093 yielding each context, the iterator will first call the prepare
1094 1094 function on each context in the window in forward order.'''
1095 1095
1096 1096 def increasing_windows(start, end, windowsize=8, sizelimit=512):
1097 1097 if start < end:
1098 1098 while start < end:
1099 1099 yield start, min(windowsize, end - start)
1100 1100 start += windowsize
1101 1101 if windowsize < sizelimit:
1102 1102 windowsize *= 2
1103 1103 else:
1104 1104 while start > end:
1105 1105 yield start, min(windowsize, start - end - 1)
1106 1106 start -= windowsize
1107 1107 if windowsize < sizelimit:
1108 1108 windowsize *= 2
1109 1109
1110 1110 follow = opts.get('follow') or opts.get('follow_first')
1111 1111
1112 1112 if not len(repo):
1113 1113 return []
1114 1114
1115 1115 if follow:
1116 1116 defrange = '%s:0' % repo['.'].rev()
1117 1117 else:
1118 1118 defrange = '-1:0'
1119 1119 revs = revrange(repo, opts['rev'] or [defrange])
1120 1120 if not revs:
1121 1121 return []
1122 1122 wanted = set()
1123 1123 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1124 1124 fncache = {}
1125 1125 change = util.cachefunc(repo.changectx)
1126 1126
1127 1127 # First step is to fill wanted, the set of revisions that we want to yield.
1128 1128 # When it does not induce extra cost, we also fill fncache for revisions in
1129 1129 # wanted: a cache of filenames that were changed (ctx.files()) and that
1130 1130 # match the file filtering conditions.
1131 1131
1132 1132 if not slowpath and not match.files():
1133 1133 # No files, no patterns. Display all revs.
1134 1134 wanted = set(revs)
1135 1135 copies = []
1136 1136
1137 1137 if not slowpath:
1138 1138 # We only have to read through the filelog to find wanted revisions
1139 1139
1140 1140 minrev, maxrev = min(revs), max(revs)
1141 1141 def filerevgen(filelog, last):
1142 1142 """
1143 1143 Only files, no patterns. Check the history of each file.
1144 1144
1145 1145 Examines filelog entries within minrev, maxrev linkrev range
1146 1146 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1147 1147 tuples in backwards order
1148 1148 """
1149 1149 cl_count = len(repo)
1150 1150 revs = []
1151 1151 for j in xrange(0, last + 1):
1152 1152 linkrev = filelog.linkrev(j)
1153 1153 if linkrev < minrev:
1154 1154 continue
1155 1155 # only yield rev for which we have the changelog, it can
1156 1156 # happen while doing "hg log" during a pull or commit
1157 1157 if linkrev >= cl_count:
1158 1158 break
1159 1159
1160 1160 parentlinkrevs = []
1161 1161 for p in filelog.parentrevs(j):
1162 1162 if p != nullrev:
1163 1163 parentlinkrevs.append(filelog.linkrev(p))
1164 1164 n = filelog.node(j)
1165 1165 revs.append((linkrev, parentlinkrevs,
1166 1166 follow and filelog.renamed(n)))
1167 1167
1168 1168 return reversed(revs)
1169 1169 def iterfiles():
1170 1170 for filename in match.files():
1171 1171 yield filename, None
1172 1172 for filename_node in copies:
1173 1173 yield filename_node
1174 1174 for file_, node in iterfiles():
1175 1175 filelog = repo.file(file_)
1176 1176 if not len(filelog):
1177 1177 if node is None:
1178 1178 # A zero count may be a directory or deleted file, so
1179 1179 # try to find matching entries on the slow path.
1180 1180 if follow:
1181 1181 raise util.Abort(
1182 1182 _('cannot follow nonexistent file: "%s"') % file_)
1183 1183 slowpath = True
1184 1184 break
1185 1185 else:
1186 1186 continue
1187 1187
1188 1188 if node is None:
1189 1189 last = len(filelog) - 1
1190 1190 else:
1191 1191 last = filelog.rev(node)
1192 1192
1193 1193
1194 1194 # keep track of all ancestors of the file
1195 1195 ancestors = set([filelog.linkrev(last)])
1196 1196
1197 1197 # iterate from latest to oldest revision
1198 1198 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1199 1199 if not follow:
1200 1200 if rev > maxrev:
1201 1201 continue
1202 1202 else:
1203 1203 # Note that last might not be the first interesting
1204 1204 # rev to us:
1205 1205 # if the file has been changed after maxrev, we'll
1206 1206 # have linkrev(last) > maxrev, and we still need
1207 1207 # to explore the file graph
1208 1208 if rev not in ancestors:
1209 1209 continue
1210 1210 # XXX insert 1327 fix here
1211 1211 if flparentlinkrevs:
1212 1212 ancestors.update(flparentlinkrevs)
1213 1213
1214 1214 fncache.setdefault(rev, []).append(file_)
1215 1215 wanted.add(rev)
1216 1216 if copied:
1217 1217 copies.append(copied)
1218 1218 if slowpath:
1219 1219 # We have to read the changelog to match filenames against
1220 1220 # changed files
1221 1221
1222 1222 if follow:
1223 1223 raise util.Abort(_('can only follow copies/renames for explicit '
1224 1224 'filenames'))
1225 1225
1226 1226 # The slow path checks files modified in every changeset.
1227 1227 for i in sorted(revs):
1228 1228 ctx = change(i)
1229 1229 matches = filter(match, ctx.files())
1230 1230 if matches:
1231 1231 fncache[i] = matches
1232 1232 wanted.add(i)
1233 1233
1234 1234 class followfilter(object):
1235 1235 def __init__(self, onlyfirst=False):
1236 1236 self.startrev = nullrev
1237 1237 self.roots = set()
1238 1238 self.onlyfirst = onlyfirst
1239 1239
1240 1240 def match(self, rev):
1241 1241 def realparents(rev):
1242 1242 if self.onlyfirst:
1243 1243 return repo.changelog.parentrevs(rev)[0:1]
1244 1244 else:
1245 1245 return filter(lambda x: x != nullrev,
1246 1246 repo.changelog.parentrevs(rev))
1247 1247
1248 1248 if self.startrev == nullrev:
1249 1249 self.startrev = rev
1250 1250 return True
1251 1251
1252 1252 if rev > self.startrev:
1253 1253 # forward: all descendants
1254 1254 if not self.roots:
1255 1255 self.roots.add(self.startrev)
1256 1256 for parent in realparents(rev):
1257 1257 if parent in self.roots:
1258 1258 self.roots.add(rev)
1259 1259 return True
1260 1260 else:
1261 1261 # backwards: all parents
1262 1262 if not self.roots:
1263 1263 self.roots.update(realparents(self.startrev))
1264 1264 if rev in self.roots:
1265 1265 self.roots.remove(rev)
1266 1266 self.roots.update(realparents(rev))
1267 1267 return True
1268 1268
1269 1269 return False
1270 1270
1271 1271 # it might be worthwhile to do this in the iterator if the rev range
1272 1272 # is descending and the prune args are all within that range
1273 1273 for rev in opts.get('prune', ()):
1274 1274 rev = repo.changelog.rev(repo.lookup(rev))
1275 1275 ff = followfilter()
1276 1276 stop = min(revs[0], revs[-1])
1277 1277 for x in xrange(rev, stop - 1, -1):
1278 1278 if ff.match(x):
1279 1279 wanted.discard(x)
1280 1280
1281 1281 # Now that wanted is correctly initialized, we can iterate over the
1282 1282 # revision range, yielding only revisions in wanted.
1283 1283 def iterate():
1284 1284 if follow and not match.files():
1285 1285 ff = followfilter(onlyfirst=opts.get('follow_first'))
1286 1286 def want(rev):
1287 1287 return ff.match(rev) and rev in wanted
1288 1288 else:
1289 1289 def want(rev):
1290 1290 return rev in wanted
1291 1291
1292 1292 for i, window in increasing_windows(0, len(revs)):
1293 1293 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1294 1294 for rev in sorted(nrevs):
1295 1295 fns = fncache.get(rev)
1296 1296 ctx = change(rev)
1297 1297 if not fns:
1298 1298 def fns_generator():
1299 1299 for f in ctx.files():
1300 1300 if match(f):
1301 1301 yield f
1302 1302 fns = fns_generator()
1303 1303 prepare(ctx, fns)
1304 1304 for rev in nrevs:
1305 1305 yield change(rev)
1306 1306 return iterate()
1307 1307
1308 1308 def add(ui, repo, match, dryrun, listsubrepos, prefix):
1309 1309 join = lambda f: os.path.join(prefix, f)
1310 1310 bad = []
1311 1311 oldbad = match.bad
1312 1312 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1313 1313 names = []
1314 1314 wctx = repo[None]
1315 1315 for f in repo.walk(match):
1316 1316 exact = match.exact(f)
1317 1317 if exact or f not in repo.dirstate:
1318 1318 names.append(f)
1319 1319 if ui.verbose or not exact:
1320 1320 ui.status(_('adding %s\n') % match.rel(join(f)))
1321 1321
1322 1322 if listsubrepos:
1323 1323 for subpath in wctx.substate:
1324 1324 sub = wctx.sub(subpath)
1325 1325 try:
1326 1326 submatch = matchmod.narrowmatcher(subpath, match)
1327 1327 bad.extend(sub.add(ui, submatch, dryrun, prefix))
1328 1328 except error.LookupError:
1329 1329 ui.status(_("skipping missing subrepository: %s\n")
1330 1330 % join(subpath))
1331 1331
1332 1332 if not dryrun:
1333 1333 rejected = wctx.add(names, prefix)
1334 1334 bad.extend(f for f in rejected if f in match.files())
1335 1335 return bad
1336 1336
1337 1337 def commit(ui, repo, commitfunc, pats, opts):
1338 1338 '''commit the specified files or all outstanding changes'''
1339 1339 date = opts.get('date')
1340 1340 if date:
1341 1341 opts['date'] = util.parsedate(date)
1342 1342 message = logmessage(opts)
1343 1343
1344 1344 # extract addremove carefully -- this function can be called from a command
1345 1345 # that doesn't support addremove
1346 1346 if opts.get('addremove'):
1347 1347 addremove(repo, pats, opts)
1348 1348
1349 1349 return commitfunc(ui, repo, message, match(repo, pats, opts), opts)
1350 1350
1351 1351 def commiteditor(repo, ctx, subs):
1352 1352 if ctx.description():
1353 1353 return ctx.description()
1354 1354 return commitforceeditor(repo, ctx, subs)
1355 1355
1356 1356 def commitforceeditor(repo, ctx, subs):
1357 1357 edittext = []
1358 1358 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1359 1359 if ctx.description():
1360 1360 edittext.append(ctx.description())
1361 1361 edittext.append("")
1362 1362 edittext.append("") # Empty line between message and comments.
1363 1363 edittext.append(_("HG: Enter commit message."
1364 1364 " Lines beginning with 'HG:' are removed."))
1365 1365 edittext.append(_("HG: Leave message empty to abort commit."))
1366 1366 edittext.append("HG: --")
1367 1367 edittext.append(_("HG: user: %s") % ctx.user())
1368 1368 if ctx.p2():
1369 1369 edittext.append(_("HG: branch merge"))
1370 1370 if ctx.branch():
1371 1371 edittext.append(_("HG: branch '%s'") % ctx.branch())
1372 1372 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1373 1373 edittext.extend([_("HG: added %s") % f for f in added])
1374 1374 edittext.extend([_("HG: changed %s") % f for f in modified])
1375 1375 edittext.extend([_("HG: removed %s") % f for f in removed])
1376 1376 if not added and not modified and not removed:
1377 1377 edittext.append(_("HG: no files changed"))
1378 1378 edittext.append("")
1379 1379 # run editor in the repository root
1380 1380 olddir = os.getcwd()
1381 1381 os.chdir(repo.root)
1382 1382 text = repo.ui.edit("\n".join(edittext), ctx.user())
1383 1383 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
1384 1384 os.chdir(olddir)
1385 1385
1386 1386 if not text.strip():
1387 1387 raise util.Abort(_("empty commit message"))
1388 1388
1389 1389 return text
General Comments 0
You need to be logged in to leave comments. Login now