##// END OF EJS Templates
remoteui: properly create dst with copy()
Matt Mackall -
r8798:92fc57c9 default
parent child Browse files
Show More
@@ -1,1244 +1,1244
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, incorporated herein by reference.
7 7
8 8 from node import hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, errno, re, glob
11 11 import mdiff, bdiff, util, templater, patch, error, encoding
12 12 import match as _match
13 13
14 14 revrangesep = ':'
15 15
16 16 def findpossible(cmd, table, strict=False):
17 17 """
18 18 Return cmd -> (aliases, command table entry)
19 19 for each matching command.
20 20 Return debug commands (or their aliases) only if no normal command matches.
21 21 """
22 22 choice = {}
23 23 debugchoice = {}
24 24 for e in table.keys():
25 25 aliases = e.lstrip("^").split("|")
26 26 found = None
27 27 if cmd in aliases:
28 28 found = cmd
29 29 elif not strict:
30 30 for a in aliases:
31 31 if a.startswith(cmd):
32 32 found = a
33 33 break
34 34 if found is not None:
35 35 if aliases[0].startswith("debug") or found.startswith("debug"):
36 36 debugchoice[found] = (aliases, table[e])
37 37 else:
38 38 choice[found] = (aliases, table[e])
39 39
40 40 if not choice and debugchoice:
41 41 choice = debugchoice
42 42
43 43 return choice
44 44
45 45 def findcmd(cmd, table, strict=True):
46 46 """Return (aliases, command table entry) for command string."""
47 47 choice = findpossible(cmd, table, strict)
48 48
49 49 if cmd in choice:
50 50 return choice[cmd]
51 51
52 52 if len(choice) > 1:
53 53 clist = choice.keys()
54 54 clist.sort()
55 55 raise error.AmbiguousCommand(cmd, clist)
56 56
57 57 if choice:
58 58 return choice.values()[0]
59 59
60 60 raise error.UnknownCommand(cmd)
61 61
62 62 def bail_if_changed(repo):
63 63 if repo.dirstate.parents()[1] != nullid:
64 64 raise util.Abort(_('outstanding uncommitted merge'))
65 65 modified, added, removed, deleted = repo.status()[:4]
66 66 if modified or added or removed or deleted:
67 67 raise util.Abort(_("outstanding uncommitted changes"))
68 68
69 69 def logmessage(opts):
70 70 """ get the log message according to -m and -l option """
71 71 message = opts.get('message')
72 72 logfile = opts.get('logfile')
73 73
74 74 if message and logfile:
75 75 raise util.Abort(_('options --message and --logfile are mutually '
76 76 'exclusive'))
77 77 if not message and logfile:
78 78 try:
79 79 if logfile == '-':
80 80 message = sys.stdin.read()
81 81 else:
82 82 message = open(logfile).read()
83 83 except IOError, inst:
84 84 raise util.Abort(_("can't read commit message '%s': %s") %
85 85 (logfile, inst.strerror))
86 86 return message
87 87
88 88 def loglimit(opts):
89 89 """get the log limit according to option -l/--limit"""
90 90 limit = opts.get('limit')
91 91 if limit:
92 92 try:
93 93 limit = int(limit)
94 94 except ValueError:
95 95 raise util.Abort(_('limit must be a positive integer'))
96 96 if limit <= 0: raise util.Abort(_('limit must be positive'))
97 97 else:
98 98 limit = sys.maxint
99 99 return limit
100 100
101 101 def remoteui(src, opts):
102 102 'build a remote ui from ui or repo and opts'
103 103 if hasattr(src, 'baseui'): # looks like a repository
104 dst = src.baseui # drop repo-specific config
104 dst = src.baseui.copy() # drop repo-specific config
105 105 src = src.ui # copy target options from repo
106 106 else: # assume it's a global ui object
107 dst = src # keep all global options
107 dst = src.copy() # keep all global options
108 108
109 109 # copy ssh-specific options
110 110 for o in 'ssh', 'remotecmd':
111 111 v = opts.get(o) or src.config('ui', o)
112 112 if v:
113 113 dst.setconfig("ui", o, v)
114 114 # copy bundle-specific options
115 115 r = src.config('bundle', 'mainreporoot')
116 116 if r:
117 117 dst.setconfig('bundle', 'mainreporoot', r)
118 118
119 119 return dst
120 120
121 121 def revpair(repo, revs):
122 122 '''return pair of nodes, given list of revisions. second item can
123 123 be None, meaning use working dir.'''
124 124
125 125 def revfix(repo, val, defval):
126 126 if not val and val != 0 and defval is not None:
127 127 val = defval
128 128 return repo.lookup(val)
129 129
130 130 if not revs:
131 131 return repo.dirstate.parents()[0], None
132 132 end = None
133 133 if len(revs) == 1:
134 134 if revrangesep in revs[0]:
135 135 start, end = revs[0].split(revrangesep, 1)
136 136 start = revfix(repo, start, 0)
137 137 end = revfix(repo, end, len(repo) - 1)
138 138 else:
139 139 start = revfix(repo, revs[0], None)
140 140 elif len(revs) == 2:
141 141 if revrangesep in revs[0] or revrangesep in revs[1]:
142 142 raise util.Abort(_('too many revisions specified'))
143 143 start = revfix(repo, revs[0], None)
144 144 end = revfix(repo, revs[1], None)
145 145 else:
146 146 raise util.Abort(_('too many revisions specified'))
147 147 return start, end
148 148
149 149 def revrange(repo, revs):
150 150 """Yield revision as strings from a list of revision specifications."""
151 151
152 152 def revfix(repo, val, defval):
153 153 if not val and val != 0 and defval is not None:
154 154 return defval
155 155 return repo.changelog.rev(repo.lookup(val))
156 156
157 157 seen, l = set(), []
158 158 for spec in revs:
159 159 if revrangesep in spec:
160 160 start, end = spec.split(revrangesep, 1)
161 161 start = revfix(repo, start, 0)
162 162 end = revfix(repo, end, len(repo) - 1)
163 163 step = start > end and -1 or 1
164 164 for rev in xrange(start, end+step, step):
165 165 if rev in seen:
166 166 continue
167 167 seen.add(rev)
168 168 l.append(rev)
169 169 else:
170 170 rev = revfix(repo, spec, None)
171 171 if rev in seen:
172 172 continue
173 173 seen.add(rev)
174 174 l.append(rev)
175 175
176 176 return l
177 177
178 178 def make_filename(repo, pat, node,
179 179 total=None, seqno=None, revwidth=None, pathname=None):
180 180 node_expander = {
181 181 'H': lambda: hex(node),
182 182 'R': lambda: str(repo.changelog.rev(node)),
183 183 'h': lambda: short(node),
184 184 }
185 185 expander = {
186 186 '%': lambda: '%',
187 187 'b': lambda: os.path.basename(repo.root),
188 188 }
189 189
190 190 try:
191 191 if node:
192 192 expander.update(node_expander)
193 193 if node:
194 194 expander['r'] = (lambda:
195 195 str(repo.changelog.rev(node)).zfill(revwidth or 0))
196 196 if total is not None:
197 197 expander['N'] = lambda: str(total)
198 198 if seqno is not None:
199 199 expander['n'] = lambda: str(seqno)
200 200 if total is not None and seqno is not None:
201 201 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
202 202 if pathname is not None:
203 203 expander['s'] = lambda: os.path.basename(pathname)
204 204 expander['d'] = lambda: os.path.dirname(pathname) or '.'
205 205 expander['p'] = lambda: pathname
206 206
207 207 newname = []
208 208 patlen = len(pat)
209 209 i = 0
210 210 while i < patlen:
211 211 c = pat[i]
212 212 if c == '%':
213 213 i += 1
214 214 c = pat[i]
215 215 c = expander[c]()
216 216 newname.append(c)
217 217 i += 1
218 218 return ''.join(newname)
219 219 except KeyError, inst:
220 220 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
221 221 inst.args[0])
222 222
223 223 def make_file(repo, pat, node=None,
224 224 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
225 225
226 226 writable = 'w' in mode or 'a' in mode
227 227
228 228 if not pat or pat == '-':
229 229 return writable and sys.stdout or sys.stdin
230 230 if hasattr(pat, 'write') and writable:
231 231 return pat
232 232 if hasattr(pat, 'read') and 'r' in mode:
233 233 return pat
234 234 return open(make_filename(repo, pat, node, total, seqno, revwidth,
235 235 pathname),
236 236 mode)
237 237
238 238 def expandpats(pats):
239 239 if not util.expandglobs:
240 240 return list(pats)
241 241 ret = []
242 242 for p in pats:
243 243 kind, name = _match._patsplit(p, None)
244 244 if kind is None:
245 245 globbed = glob.glob(name)
246 246 if globbed:
247 247 ret.extend(globbed)
248 248 continue
249 249 ret.append(p)
250 250 return ret
251 251
252 252 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
253 253 if not globbed and default == 'relpath':
254 254 pats = expandpats(pats or [])
255 255 m = _match.match(repo.root, repo.getcwd(), pats,
256 256 opts.get('include'), opts.get('exclude'), default)
257 257 def badfn(f, msg):
258 258 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
259 259 m.bad = badfn
260 260 return m
261 261
262 262 def matchall(repo):
263 263 return _match.always(repo.root, repo.getcwd())
264 264
265 265 def matchfiles(repo, files):
266 266 return _match.exact(repo.root, repo.getcwd(), files)
267 267
268 268 def findrenames(repo, match=None, threshold=0.5):
269 269 '''find renamed files -- yields (before, after, score) tuples'''
270 270 added, removed = repo.status(match=match)[1:3]
271 271 ctx = repo['.']
272 272 for a in added:
273 273 aa = repo.wread(a)
274 274 bestname, bestscore = None, threshold
275 275 for r in removed:
276 276 rr = ctx.filectx(r).data()
277 277
278 278 # bdiff.blocks() returns blocks of matching lines
279 279 # count the number of bytes in each
280 280 equal = 0
281 281 alines = mdiff.splitnewlines(aa)
282 282 matches = bdiff.blocks(aa, rr)
283 283 for x1,x2,y1,y2 in matches:
284 284 for line in alines[x1:x2]:
285 285 equal += len(line)
286 286
287 287 lengths = len(aa) + len(rr)
288 288 if lengths:
289 289 myscore = equal*2.0 / lengths
290 290 if myscore >= bestscore:
291 291 bestname, bestscore = r, myscore
292 292 if bestname:
293 293 yield bestname, a, bestscore
294 294
295 295 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
296 296 if dry_run is None:
297 297 dry_run = opts.get('dry_run')
298 298 if similarity is None:
299 299 similarity = float(opts.get('similarity') or 0)
300 300 add, remove = [], []
301 301 audit_path = util.path_auditor(repo.root)
302 302 m = match(repo, pats, opts)
303 303 for abs in repo.walk(m):
304 304 target = repo.wjoin(abs)
305 305 good = True
306 306 try:
307 307 audit_path(abs)
308 308 except:
309 309 good = False
310 310 rel = m.rel(abs)
311 311 exact = m.exact(abs)
312 312 if good and abs not in repo.dirstate:
313 313 add.append(abs)
314 314 if repo.ui.verbose or not exact:
315 315 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
316 316 if repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
317 317 or (os.path.isdir(target) and not os.path.islink(target))):
318 318 remove.append(abs)
319 319 if repo.ui.verbose or not exact:
320 320 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
321 321 if not dry_run:
322 322 repo.remove(remove)
323 323 repo.add(add)
324 324 if similarity > 0:
325 325 for old, new, score in findrenames(repo, m, similarity):
326 326 oldexact, newexact = m.exact(old), m.exact(new)
327 327 if repo.ui.verbose or not oldexact or not newexact:
328 328 oldrel, newrel = m.rel(old), m.rel(new)
329 329 repo.ui.status(_('recording removal of %s as rename to %s '
330 330 '(%d%% similar)\n') %
331 331 (oldrel, newrel, score * 100))
332 332 if not dry_run:
333 333 repo.copy(old, new)
334 334
335 335 def copy(ui, repo, pats, opts, rename=False):
336 336 # called with the repo lock held
337 337 #
338 338 # hgsep => pathname that uses "/" to separate directories
339 339 # ossep => pathname that uses os.sep to separate directories
340 340 cwd = repo.getcwd()
341 341 targets = {}
342 342 after = opts.get("after")
343 343 dryrun = opts.get("dry_run")
344 344
345 345 def walkpat(pat):
346 346 srcs = []
347 347 m = match(repo, [pat], opts, globbed=True)
348 348 for abs in repo.walk(m):
349 349 state = repo.dirstate[abs]
350 350 rel = m.rel(abs)
351 351 exact = m.exact(abs)
352 352 if state in '?r':
353 353 if exact and state == '?':
354 354 ui.warn(_('%s: not copying - file is not managed\n') % rel)
355 355 if exact and state == 'r':
356 356 ui.warn(_('%s: not copying - file has been marked for'
357 357 ' remove\n') % rel)
358 358 continue
359 359 # abs: hgsep
360 360 # rel: ossep
361 361 srcs.append((abs, rel, exact))
362 362 return srcs
363 363
364 364 # abssrc: hgsep
365 365 # relsrc: ossep
366 366 # otarget: ossep
367 367 def copyfile(abssrc, relsrc, otarget, exact):
368 368 abstarget = util.canonpath(repo.root, cwd, otarget)
369 369 reltarget = repo.pathto(abstarget, cwd)
370 370 target = repo.wjoin(abstarget)
371 371 src = repo.wjoin(abssrc)
372 372 state = repo.dirstate[abstarget]
373 373
374 374 # check for collisions
375 375 prevsrc = targets.get(abstarget)
376 376 if prevsrc is not None:
377 377 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
378 378 (reltarget, repo.pathto(abssrc, cwd),
379 379 repo.pathto(prevsrc, cwd)))
380 380 return
381 381
382 382 # check for overwrites
383 383 exists = os.path.exists(target)
384 384 if not after and exists or after and state in 'mn':
385 385 if not opts['force']:
386 386 ui.warn(_('%s: not overwriting - file exists\n') %
387 387 reltarget)
388 388 return
389 389
390 390 if after:
391 391 if not exists:
392 392 return
393 393 elif not dryrun:
394 394 try:
395 395 if exists:
396 396 os.unlink(target)
397 397 targetdir = os.path.dirname(target) or '.'
398 398 if not os.path.isdir(targetdir):
399 399 os.makedirs(targetdir)
400 400 util.copyfile(src, target)
401 401 except IOError, inst:
402 402 if inst.errno == errno.ENOENT:
403 403 ui.warn(_('%s: deleted in working copy\n') % relsrc)
404 404 else:
405 405 ui.warn(_('%s: cannot copy - %s\n') %
406 406 (relsrc, inst.strerror))
407 407 return True # report a failure
408 408
409 409 if ui.verbose or not exact:
410 410 if rename:
411 411 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
412 412 else:
413 413 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
414 414
415 415 targets[abstarget] = abssrc
416 416
417 417 # fix up dirstate
418 418 origsrc = repo.dirstate.copied(abssrc) or abssrc
419 419 if abstarget == origsrc: # copying back a copy?
420 420 if state not in 'mn' and not dryrun:
421 421 repo.dirstate.normallookup(abstarget)
422 422 else:
423 423 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
424 424 if not ui.quiet:
425 425 ui.warn(_("%s has not been committed yet, so no copy "
426 426 "data will be stored for %s.\n")
427 427 % (repo.pathto(origsrc, cwd), reltarget))
428 428 if repo.dirstate[abstarget] in '?r' and not dryrun:
429 429 repo.add([abstarget])
430 430 elif not dryrun:
431 431 repo.copy(origsrc, abstarget)
432 432
433 433 if rename and not dryrun:
434 434 repo.remove([abssrc], not after)
435 435
436 436 # pat: ossep
437 437 # dest ossep
438 438 # srcs: list of (hgsep, hgsep, ossep, bool)
439 439 # return: function that takes hgsep and returns ossep
440 440 def targetpathfn(pat, dest, srcs):
441 441 if os.path.isdir(pat):
442 442 abspfx = util.canonpath(repo.root, cwd, pat)
443 443 abspfx = util.localpath(abspfx)
444 444 if destdirexists:
445 445 striplen = len(os.path.split(abspfx)[0])
446 446 else:
447 447 striplen = len(abspfx)
448 448 if striplen:
449 449 striplen += len(os.sep)
450 450 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
451 451 elif destdirexists:
452 452 res = lambda p: os.path.join(dest,
453 453 os.path.basename(util.localpath(p)))
454 454 else:
455 455 res = lambda p: dest
456 456 return res
457 457
458 458 # pat: ossep
459 459 # dest ossep
460 460 # srcs: list of (hgsep, hgsep, ossep, bool)
461 461 # return: function that takes hgsep and returns ossep
462 462 def targetpathafterfn(pat, dest, srcs):
463 463 if _match.patkind(pat):
464 464 # a mercurial pattern
465 465 res = lambda p: os.path.join(dest,
466 466 os.path.basename(util.localpath(p)))
467 467 else:
468 468 abspfx = util.canonpath(repo.root, cwd, pat)
469 469 if len(abspfx) < len(srcs[0][0]):
470 470 # A directory. Either the target path contains the last
471 471 # component of the source path or it does not.
472 472 def evalpath(striplen):
473 473 score = 0
474 474 for s in srcs:
475 475 t = os.path.join(dest, util.localpath(s[0])[striplen:])
476 476 if os.path.exists(t):
477 477 score += 1
478 478 return score
479 479
480 480 abspfx = util.localpath(abspfx)
481 481 striplen = len(abspfx)
482 482 if striplen:
483 483 striplen += len(os.sep)
484 484 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
485 485 score = evalpath(striplen)
486 486 striplen1 = len(os.path.split(abspfx)[0])
487 487 if striplen1:
488 488 striplen1 += len(os.sep)
489 489 if evalpath(striplen1) > score:
490 490 striplen = striplen1
491 491 res = lambda p: os.path.join(dest,
492 492 util.localpath(p)[striplen:])
493 493 else:
494 494 # a file
495 495 if destdirexists:
496 496 res = lambda p: os.path.join(dest,
497 497 os.path.basename(util.localpath(p)))
498 498 else:
499 499 res = lambda p: dest
500 500 return res
501 501
502 502
503 503 pats = expandpats(pats)
504 504 if not pats:
505 505 raise util.Abort(_('no source or destination specified'))
506 506 if len(pats) == 1:
507 507 raise util.Abort(_('no destination specified'))
508 508 dest = pats.pop()
509 509 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
510 510 if not destdirexists:
511 511 if len(pats) > 1 or _match.patkind(pats[0]):
512 512 raise util.Abort(_('with multiple sources, destination must be an '
513 513 'existing directory'))
514 514 if util.endswithsep(dest):
515 515 raise util.Abort(_('destination %s is not a directory') % dest)
516 516
517 517 tfn = targetpathfn
518 518 if after:
519 519 tfn = targetpathafterfn
520 520 copylist = []
521 521 for pat in pats:
522 522 srcs = walkpat(pat)
523 523 if not srcs:
524 524 continue
525 525 copylist.append((tfn(pat, dest, srcs), srcs))
526 526 if not copylist:
527 527 raise util.Abort(_('no files to copy'))
528 528
529 529 errors = 0
530 530 for targetpath, srcs in copylist:
531 531 for abssrc, relsrc, exact in srcs:
532 532 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
533 533 errors += 1
534 534
535 535 if errors:
536 536 ui.warn(_('(consider using --after)\n'))
537 537
538 538 return errors
539 539
540 540 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None):
541 541 '''Run a command as a service.'''
542 542
543 543 if opts['daemon'] and not opts['daemon_pipefds']:
544 544 rfd, wfd = os.pipe()
545 545 args = sys.argv[:]
546 546 args.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
547 547 # Don't pass --cwd to the child process, because we've already
548 548 # changed directory.
549 549 for i in xrange(1,len(args)):
550 550 if args[i].startswith('--cwd='):
551 551 del args[i]
552 552 break
553 553 elif args[i].startswith('--cwd'):
554 554 del args[i:i+2]
555 555 break
556 556 pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
557 557 args[0], args)
558 558 os.close(wfd)
559 559 os.read(rfd, 1)
560 560 if parentfn:
561 561 return parentfn(pid)
562 562 else:
563 563 os._exit(0)
564 564
565 565 if initfn:
566 566 initfn()
567 567
568 568 if opts['pid_file']:
569 569 fp = open(opts['pid_file'], 'w')
570 570 fp.write(str(os.getpid()) + '\n')
571 571 fp.close()
572 572
573 573 if opts['daemon_pipefds']:
574 574 rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')]
575 575 os.close(rfd)
576 576 try:
577 577 os.setsid()
578 578 except AttributeError:
579 579 pass
580 580 os.write(wfd, 'y')
581 581 os.close(wfd)
582 582 sys.stdout.flush()
583 583 sys.stderr.flush()
584 584
585 585 nullfd = os.open(util.nulldev, os.O_RDWR)
586 586 logfilefd = nullfd
587 587 if logfile:
588 588 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
589 589 os.dup2(nullfd, 0)
590 590 os.dup2(logfilefd, 1)
591 591 os.dup2(logfilefd, 2)
592 592 if nullfd not in (0, 1, 2):
593 593 os.close(nullfd)
594 594 if logfile and logfilefd not in (0, 1, 2):
595 595 os.close(logfilefd)
596 596
597 597 if runfn:
598 598 return runfn()
599 599
600 600 class changeset_printer(object):
601 601 '''show changeset information when templating not requested.'''
602 602
603 603 def __init__(self, ui, repo, patch, diffopts, buffered):
604 604 self.ui = ui
605 605 self.repo = repo
606 606 self.buffered = buffered
607 607 self.patch = patch
608 608 self.diffopts = diffopts
609 609 self.header = {}
610 610 self.hunk = {}
611 611 self.lastheader = None
612 612
613 613 def flush(self, rev):
614 614 if rev in self.header:
615 615 h = self.header[rev]
616 616 if h != self.lastheader:
617 617 self.lastheader = h
618 618 self.ui.write(h)
619 619 del self.header[rev]
620 620 if rev in self.hunk:
621 621 self.ui.write(self.hunk[rev])
622 622 del self.hunk[rev]
623 623 return 1
624 624 return 0
625 625
626 626 def show(self, ctx, copies=(), **props):
627 627 if self.buffered:
628 628 self.ui.pushbuffer()
629 629 self._show(ctx, copies, props)
630 630 self.hunk[ctx.rev()] = self.ui.popbuffer()
631 631 else:
632 632 self._show(ctx, copies, props)
633 633
634 634 def _show(self, ctx, copies, props):
635 635 '''show a single changeset or file revision'''
636 636 changenode = ctx.node()
637 637 rev = ctx.rev()
638 638
639 639 if self.ui.quiet:
640 640 self.ui.write("%d:%s\n" % (rev, short(changenode)))
641 641 return
642 642
643 643 log = self.repo.changelog
644 644 changes = log.read(changenode)
645 645 date = util.datestr(changes[2])
646 646 extra = changes[5]
647 647 branch = extra.get("branch")
648 648
649 649 hexfunc = self.ui.debugflag and hex or short
650 650
651 651 parents = [(p, hexfunc(log.node(p)))
652 652 for p in self._meaningful_parentrevs(log, rev)]
653 653
654 654 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
655 655
656 656 # don't show the default branch name
657 657 if branch != 'default':
658 658 branch = encoding.tolocal(branch)
659 659 self.ui.write(_("branch: %s\n") % branch)
660 660 for tag in self.repo.nodetags(changenode):
661 661 self.ui.write(_("tag: %s\n") % tag)
662 662 for parent in parents:
663 663 self.ui.write(_("parent: %d:%s\n") % parent)
664 664
665 665 if self.ui.debugflag:
666 666 self.ui.write(_("manifest: %d:%s\n") %
667 667 (self.repo.manifest.rev(changes[0]), hex(changes[0])))
668 668 self.ui.write(_("user: %s\n") % changes[1])
669 669 self.ui.write(_("date: %s\n") % date)
670 670
671 671 if self.ui.debugflag:
672 672 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
673 673 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
674 674 files):
675 675 if value:
676 676 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
677 677 elif changes[3] and self.ui.verbose:
678 678 self.ui.write(_("files: %s\n") % " ".join(changes[3]))
679 679 if copies and self.ui.verbose:
680 680 copies = ['%s (%s)' % c for c in copies]
681 681 self.ui.write(_("copies: %s\n") % ' '.join(copies))
682 682
683 683 if extra and self.ui.debugflag:
684 684 for key, value in sorted(extra.items()):
685 685 self.ui.write(_("extra: %s=%s\n")
686 686 % (key, value.encode('string_escape')))
687 687
688 688 description = changes[4].strip()
689 689 if description:
690 690 if self.ui.verbose:
691 691 self.ui.write(_("description:\n"))
692 692 self.ui.write(description)
693 693 self.ui.write("\n\n")
694 694 else:
695 695 self.ui.write(_("summary: %s\n") %
696 696 description.splitlines()[0])
697 697 self.ui.write("\n")
698 698
699 699 self.showpatch(changenode)
700 700
701 701 def showpatch(self, node):
702 702 if self.patch:
703 703 prev = self.repo.changelog.parents(node)[0]
704 704 chunks = patch.diff(self.repo, prev, node, match=self.patch,
705 705 opts=patch.diffopts(self.ui, self.diffopts))
706 706 for chunk in chunks:
707 707 self.ui.write(chunk)
708 708 self.ui.write("\n")
709 709
710 710 def _meaningful_parentrevs(self, log, rev):
711 711 """Return list of meaningful (or all if debug) parentrevs for rev.
712 712
713 713 For merges (two non-nullrev revisions) both parents are meaningful.
714 714 Otherwise the first parent revision is considered meaningful if it
715 715 is not the preceding revision.
716 716 """
717 717 parents = log.parentrevs(rev)
718 718 if not self.ui.debugflag and parents[1] == nullrev:
719 719 if parents[0] >= rev - 1:
720 720 parents = []
721 721 else:
722 722 parents = [parents[0]]
723 723 return parents
724 724
725 725
726 726 class changeset_templater(changeset_printer):
727 727 '''format changeset information.'''
728 728
729 729 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
730 730 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
731 731 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
732 732 self.t = templater.templater(mapfile, {'formatnode': formatnode},
733 733 cache={
734 734 'parent': '{rev}:{node|formatnode} ',
735 735 'manifest': '{rev}:{node|formatnode}',
736 736 'filecopy': '{name} ({source})'})
737 737
738 738 def use_template(self, t):
739 739 '''set template string to use'''
740 740 self.t.cache['changeset'] = t
741 741
742 742 def _meaningful_parentrevs(self, ctx):
743 743 """Return list of meaningful (or all if debug) parentrevs for rev.
744 744 """
745 745 parents = ctx.parents()
746 746 if len(parents) > 1:
747 747 return parents
748 748 if self.ui.debugflag:
749 749 return [parents[0], self.repo['null']]
750 750 if parents[0].rev() >= ctx.rev() - 1:
751 751 return []
752 752 return parents
753 753
754 754 def _show(self, ctx, copies, props):
755 755 '''show a single changeset or file revision'''
756 756
757 757 def showlist(name, values, plural=None, **args):
758 758 '''expand set of values.
759 759 name is name of key in template map.
760 760 values is list of strings or dicts.
761 761 plural is plural of name, if not simply name + 's'.
762 762
763 763 expansion works like this, given name 'foo'.
764 764
765 765 if values is empty, expand 'no_foos'.
766 766
767 767 if 'foo' not in template map, return values as a string,
768 768 joined by space.
769 769
770 770 expand 'start_foos'.
771 771
772 772 for each value, expand 'foo'. if 'last_foo' in template
773 773 map, expand it instead of 'foo' for last key.
774 774
775 775 expand 'end_foos'.
776 776 '''
777 777 if plural: names = plural
778 778 else: names = name + 's'
779 779 if not values:
780 780 noname = 'no_' + names
781 781 if noname in self.t:
782 782 yield self.t(noname, **args)
783 783 return
784 784 if name not in self.t:
785 785 if isinstance(values[0], str):
786 786 yield ' '.join(values)
787 787 else:
788 788 for v in values:
789 789 yield dict(v, **args)
790 790 return
791 791 startname = 'start_' + names
792 792 if startname in self.t:
793 793 yield self.t(startname, **args)
794 794 vargs = args.copy()
795 795 def one(v, tag=name):
796 796 try:
797 797 vargs.update(v)
798 798 except (AttributeError, ValueError):
799 799 try:
800 800 for a, b in v:
801 801 vargs[a] = b
802 802 except ValueError:
803 803 vargs[name] = v
804 804 return self.t(tag, **vargs)
805 805 lastname = 'last_' + name
806 806 if lastname in self.t:
807 807 last = values.pop()
808 808 else:
809 809 last = None
810 810 for v in values:
811 811 yield one(v)
812 812 if last is not None:
813 813 yield one(last, tag=lastname)
814 814 endname = 'end_' + names
815 815 if endname in self.t:
816 816 yield self.t(endname, **args)
817 817
818 818 def showbranches(**args):
819 819 branch = ctx.branch()
820 820 if branch != 'default':
821 821 branch = encoding.tolocal(branch)
822 822 return showlist('branch', [branch], plural='branches', **args)
823 823
824 824 def showparents(**args):
825 825 parents = [[('rev', p.rev()), ('node', p.hex())]
826 826 for p in self._meaningful_parentrevs(ctx)]
827 827 return showlist('parent', parents, **args)
828 828
829 829 def showtags(**args):
830 830 return showlist('tag', ctx.tags(), **args)
831 831
832 832 def showextras(**args):
833 833 for key, value in sorted(ctx.extra().items()):
834 834 args = args.copy()
835 835 args.update(dict(key=key, value=value))
836 836 yield self.t('extra', **args)
837 837
838 838 def showcopies(**args):
839 839 c = [{'name': x[0], 'source': x[1]} for x in copies]
840 840 return showlist('file_copy', c, plural='file_copies', **args)
841 841
842 842 files = []
843 843 def getfiles():
844 844 if not files:
845 845 files[:] = self.repo.status(ctx.parents()[0].node(),
846 846 ctx.node())[:3]
847 847 return files
848 848 def showfiles(**args):
849 849 return showlist('file', ctx.files(), **args)
850 850 def showmods(**args):
851 851 return showlist('file_mod', getfiles()[0], **args)
852 852 def showadds(**args):
853 853 return showlist('file_add', getfiles()[1], **args)
854 854 def showdels(**args):
855 855 return showlist('file_del', getfiles()[2], **args)
856 856 def showmanifest(**args):
857 857 args = args.copy()
858 858 args.update(dict(rev=self.repo.manifest.rev(ctx.changeset()[0]),
859 859 node=hex(ctx.changeset()[0])))
860 860 return self.t('manifest', **args)
861 861
862 862 def showdiffstat(**args):
863 863 diff = patch.diff(self.repo, ctx.parents()[0].node(), ctx.node())
864 864 files, adds, removes = 0, 0, 0
865 865 for i in patch.diffstatdata(util.iterlines(diff)):
866 866 files += 1
867 867 adds += i[1]
868 868 removes += i[2]
869 869 return '%s: +%s/-%s' % (files, adds, removes)
870 870
871 871 defprops = {
872 872 'author': ctx.user(),
873 873 'branches': showbranches,
874 874 'date': ctx.date(),
875 875 'desc': ctx.description().strip(),
876 876 'file_adds': showadds,
877 877 'file_dels': showdels,
878 878 'file_mods': showmods,
879 879 'files': showfiles,
880 880 'file_copies': showcopies,
881 881 'manifest': showmanifest,
882 882 'node': ctx.hex(),
883 883 'parents': showparents,
884 884 'rev': ctx.rev(),
885 885 'tags': showtags,
886 886 'extras': showextras,
887 887 'diffstat': showdiffstat,
888 888 }
889 889 props = props.copy()
890 890 props.update(defprops)
891 891
892 892 # find correct templates for current mode
893 893
894 894 tmplmodes = [
895 895 (True, None),
896 896 (self.ui.verbose, 'verbose'),
897 897 (self.ui.quiet, 'quiet'),
898 898 (self.ui.debugflag, 'debug'),
899 899 ]
900 900
901 901 types = {'header': '', 'changeset': 'changeset'}
902 902 for mode, postfix in tmplmodes:
903 903 for type in types:
904 904 cur = postfix and ('%s_%s' % (type, postfix)) or type
905 905 if mode and cur in self.t:
906 906 types[type] = cur
907 907
908 908 try:
909 909
910 910 # write header
911 911 if types['header']:
912 912 h = templater.stringify(self.t(types['header'], **props))
913 913 if self.buffered:
914 914 self.header[ctx.rev()] = h
915 915 else:
916 916 self.ui.write(h)
917 917
918 918 # write changeset metadata, then patch if requested
919 919 key = types['changeset']
920 920 self.ui.write(templater.stringify(self.t(key, **props)))
921 921 self.showpatch(ctx.node())
922 922
923 923 except KeyError, inst:
924 924 msg = _("%s: no key named '%s'")
925 925 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
926 926 except SyntaxError, inst:
927 927 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
928 928
929 929 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
930 930 """show one changeset using template or regular display.
931 931
932 932 Display format will be the first non-empty hit of:
933 933 1. option 'template'
934 934 2. option 'style'
935 935 3. [ui] setting 'logtemplate'
936 936 4. [ui] setting 'style'
937 937 If all of these values are either the unset or the empty string,
938 938 regular display via changeset_printer() is done.
939 939 """
940 940 # options
941 941 patch = False
942 942 if opts.get('patch'):
943 943 patch = matchfn or matchall(repo)
944 944
945 945 tmpl = opts.get('template')
946 946 style = None
947 947 if tmpl:
948 948 tmpl = templater.parsestring(tmpl, quoted=False)
949 949 else:
950 950 style = opts.get('style')
951 951
952 952 # ui settings
953 953 if not (tmpl or style):
954 954 tmpl = ui.config('ui', 'logtemplate')
955 955 if tmpl:
956 956 tmpl = templater.parsestring(tmpl)
957 957 else:
958 958 style = ui.config('ui', 'style')
959 959
960 960 if not (tmpl or style):
961 961 return changeset_printer(ui, repo, patch, opts, buffered)
962 962
963 963 mapfile = None
964 964 if style and not tmpl:
965 965 mapfile = style
966 966 if not os.path.split(mapfile)[0]:
967 967 mapname = (templater.templatepath('map-cmdline.' + mapfile)
968 968 or templater.templatepath(mapfile))
969 969 if mapname: mapfile = mapname
970 970
971 971 try:
972 972 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
973 973 except SyntaxError, inst:
974 974 raise util.Abort(inst.args[0])
975 975 if tmpl: t.use_template(tmpl)
976 976 return t
977 977
978 978 def finddate(ui, repo, date):
979 979 """Find the tipmost changeset that matches the given date spec"""
980 980 df = util.matchdate(date)
981 981 get = util.cachefunc(lambda r: repo[r].changeset())
982 982 changeiter, matchfn = walkchangerevs(ui, repo, [], get, {'rev':None})
983 983 results = {}
984 984 for st, rev, fns in changeiter:
985 985 if st == 'add':
986 986 d = get(rev)[2]
987 987 if df(d[0]):
988 988 results[rev] = d
989 989 elif st == 'iter':
990 990 if rev in results:
991 991 ui.status(_("Found revision %s from %s\n") %
992 992 (rev, util.datestr(results[rev])))
993 993 return str(rev)
994 994
995 995 raise util.Abort(_("revision matching date not found"))
996 996
997 997 def walkchangerevs(ui, repo, pats, change, opts):
998 998 '''Iterate over files and the revs in which they changed.
999 999
1000 1000 Callers most commonly need to iterate backwards over the history
1001 1001 in which they are interested. Doing so has awful (quadratic-looking)
1002 1002 performance, so we use iterators in a "windowed" way.
1003 1003
1004 1004 We walk a window of revisions in the desired order. Within the
1005 1005 window, we first walk forwards to gather data, then in the desired
1006 1006 order (usually backwards) to display it.
1007 1007
1008 1008 This function returns an (iterator, matchfn) tuple. The iterator
1009 1009 yields 3-tuples. They will be of one of the following forms:
1010 1010
1011 1011 "window", incrementing, lastrev: stepping through a window,
1012 1012 positive if walking forwards through revs, last rev in the
1013 1013 sequence iterated over - use to reset state for the current window
1014 1014
1015 1015 "add", rev, fns: out-of-order traversal of the given filenames
1016 1016 fns, which changed during revision rev - use to gather data for
1017 1017 possible display
1018 1018
1019 1019 "iter", rev, None: in-order traversal of the revs earlier iterated
1020 1020 over with "add" - use to display data'''
1021 1021
1022 1022 def increasing_windows(start, end, windowsize=8, sizelimit=512):
1023 1023 if start < end:
1024 1024 while start < end:
1025 1025 yield start, min(windowsize, end-start)
1026 1026 start += windowsize
1027 1027 if windowsize < sizelimit:
1028 1028 windowsize *= 2
1029 1029 else:
1030 1030 while start > end:
1031 1031 yield start, min(windowsize, start-end-1)
1032 1032 start -= windowsize
1033 1033 if windowsize < sizelimit:
1034 1034 windowsize *= 2
1035 1035
1036 1036 m = match(repo, pats, opts)
1037 1037 follow = opts.get('follow') or opts.get('follow_first')
1038 1038
1039 1039 if not len(repo):
1040 1040 return [], m
1041 1041
1042 1042 if follow:
1043 1043 defrange = '%s:0' % repo['.'].rev()
1044 1044 else:
1045 1045 defrange = '-1:0'
1046 1046 revs = revrange(repo, opts['rev'] or [defrange])
1047 1047 wanted = set()
1048 1048 slowpath = m.anypats() or (m.files() and opts.get('removed'))
1049 1049 fncache = {}
1050 1050
1051 1051 if not slowpath and not m.files():
1052 1052 # No files, no patterns. Display all revs.
1053 1053 wanted = set(revs)
1054 1054 copies = []
1055 1055 if not slowpath:
1056 1056 # Only files, no patterns. Check the history of each file.
1057 1057 def filerevgen(filelog, node):
1058 1058 cl_count = len(repo)
1059 1059 if node is None:
1060 1060 last = len(filelog) - 1
1061 1061 else:
1062 1062 last = filelog.rev(node)
1063 1063 for i, window in increasing_windows(last, nullrev):
1064 1064 revs = []
1065 1065 for j in xrange(i - window, i + 1):
1066 1066 n = filelog.node(j)
1067 1067 revs.append((filelog.linkrev(j),
1068 1068 follow and filelog.renamed(n)))
1069 1069 for rev in reversed(revs):
1070 1070 # only yield rev for which we have the changelog, it can
1071 1071 # happen while doing "hg log" during a pull or commit
1072 1072 if rev[0] < cl_count:
1073 1073 yield rev
1074 1074 def iterfiles():
1075 1075 for filename in m.files():
1076 1076 yield filename, None
1077 1077 for filename_node in copies:
1078 1078 yield filename_node
1079 1079 minrev, maxrev = min(revs), max(revs)
1080 1080 for file_, node in iterfiles():
1081 1081 filelog = repo.file(file_)
1082 1082 if not len(filelog):
1083 1083 if node is None:
1084 1084 # A zero count may be a directory or deleted file, so
1085 1085 # try to find matching entries on the slow path.
1086 1086 if follow:
1087 1087 raise util.Abort(_('cannot follow nonexistent file: "%s"') % file_)
1088 1088 slowpath = True
1089 1089 break
1090 1090 else:
1091 1091 ui.warn(_('%s:%s copy source revision cannot be found!\n')
1092 1092 % (file_, short(node)))
1093 1093 continue
1094 1094 for rev, copied in filerevgen(filelog, node):
1095 1095 if rev <= maxrev:
1096 1096 if rev < minrev:
1097 1097 break
1098 1098 fncache.setdefault(rev, [])
1099 1099 fncache[rev].append(file_)
1100 1100 wanted.add(rev)
1101 1101 if follow and copied:
1102 1102 copies.append(copied)
1103 1103 if slowpath:
1104 1104 if follow:
1105 1105 raise util.Abort(_('can only follow copies/renames for explicit '
1106 1106 'filenames'))
1107 1107
1108 1108 # The slow path checks files modified in every changeset.
1109 1109 def changerevgen():
1110 1110 for i, window in increasing_windows(len(repo) - 1, nullrev):
1111 1111 for j in xrange(i - window, i + 1):
1112 1112 yield j, change(j)[3]
1113 1113
1114 1114 for rev, changefiles in changerevgen():
1115 1115 matches = filter(m, changefiles)
1116 1116 if matches:
1117 1117 fncache[rev] = matches
1118 1118 wanted.add(rev)
1119 1119
1120 1120 class followfilter(object):
1121 1121 def __init__(self, onlyfirst=False):
1122 1122 self.startrev = nullrev
1123 1123 self.roots = []
1124 1124 self.onlyfirst = onlyfirst
1125 1125
1126 1126 def match(self, rev):
1127 1127 def realparents(rev):
1128 1128 if self.onlyfirst:
1129 1129 return repo.changelog.parentrevs(rev)[0:1]
1130 1130 else:
1131 1131 return filter(lambda x: x != nullrev,
1132 1132 repo.changelog.parentrevs(rev))
1133 1133
1134 1134 if self.startrev == nullrev:
1135 1135 self.startrev = rev
1136 1136 return True
1137 1137
1138 1138 if rev > self.startrev:
1139 1139 # forward: all descendants
1140 1140 if not self.roots:
1141 1141 self.roots.append(self.startrev)
1142 1142 for parent in realparents(rev):
1143 1143 if parent in self.roots:
1144 1144 self.roots.append(rev)
1145 1145 return True
1146 1146 else:
1147 1147 # backwards: all parents
1148 1148 if not self.roots:
1149 1149 self.roots.extend(realparents(self.startrev))
1150 1150 if rev in self.roots:
1151 1151 self.roots.remove(rev)
1152 1152 self.roots.extend(realparents(rev))
1153 1153 return True
1154 1154
1155 1155 return False
1156 1156
1157 1157 # it might be worthwhile to do this in the iterator if the rev range
1158 1158 # is descending and the prune args are all within that range
1159 1159 for rev in opts.get('prune', ()):
1160 1160 rev = repo.changelog.rev(repo.lookup(rev))
1161 1161 ff = followfilter()
1162 1162 stop = min(revs[0], revs[-1])
1163 1163 for x in xrange(rev, stop-1, -1):
1164 1164 if ff.match(x):
1165 1165 wanted.discard(x)
1166 1166
1167 1167 def iterate():
1168 1168 if follow and not m.files():
1169 1169 ff = followfilter(onlyfirst=opts.get('follow_first'))
1170 1170 def want(rev):
1171 1171 return ff.match(rev) and rev in wanted
1172 1172 else:
1173 1173 def want(rev):
1174 1174 return rev in wanted
1175 1175
1176 1176 for i, window in increasing_windows(0, len(revs)):
1177 1177 yield 'window', revs[0] < revs[-1], revs[-1]
1178 1178 nrevs = [rev for rev in revs[i:i+window] if want(rev)]
1179 1179 for rev in sorted(nrevs):
1180 1180 fns = fncache.get(rev)
1181 1181 if not fns:
1182 1182 def fns_generator():
1183 1183 for f in change(rev)[3]:
1184 1184 if m(f):
1185 1185 yield f
1186 1186 fns = fns_generator()
1187 1187 yield 'add', rev, fns
1188 1188 for rev in nrevs:
1189 1189 yield 'iter', rev, None
1190 1190 return iterate(), m
1191 1191
1192 1192 def commit(ui, repo, commitfunc, pats, opts):
1193 1193 '''commit the specified files or all outstanding changes'''
1194 1194 date = opts.get('date')
1195 1195 if date:
1196 1196 opts['date'] = util.parsedate(date)
1197 1197 message = logmessage(opts)
1198 1198
1199 1199 # extract addremove carefully -- this function can be called from a command
1200 1200 # that doesn't support addremove
1201 1201 if opts.get('addremove'):
1202 1202 addremove(repo, pats, opts)
1203 1203
1204 1204 return commitfunc(ui, repo, message, match(repo, pats, opts), opts)
1205 1205
1206 1206 def commiteditor(repo, ctx):
1207 1207 if ctx.description():
1208 1208 return ctx.description()
1209 1209 return commitforceeditor(repo, ctx)
1210 1210
1211 1211 def commitforceeditor(repo, ctx):
1212 1212 edittext = []
1213 1213 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1214 1214 if ctx.description():
1215 1215 edittext.append(ctx.description())
1216 1216 edittext.append("")
1217 1217 edittext.append("") # Empty line between message and comments.
1218 1218 edittext.append(_("HG: Enter commit message."
1219 1219 " Lines beginning with 'HG:' are removed."))
1220 1220 edittext.append(_("HG: Leave message empty to abort commit."))
1221 1221 edittext.append("HG: --")
1222 1222 edittext.append(_("HG: user: %s") % ctx.user())
1223 1223 if ctx.p2():
1224 1224 edittext.append(_("HG: branch merge"))
1225 1225 if ctx.branch():
1226 1226 edittext.append(_("HG: branch '%s'")
1227 1227 % encoding.tolocal(ctx.branch()))
1228 1228 edittext.extend([_("HG: added %s") % f for f in added])
1229 1229 edittext.extend([_("HG: changed %s") % f for f in modified])
1230 1230 edittext.extend([_("HG: removed %s") % f for f in removed])
1231 1231 if not added and not modified and not removed:
1232 1232 edittext.append(_("HG: no files changed"))
1233 1233 edittext.append("")
1234 1234 # run editor in the repository root
1235 1235 olddir = os.getcwd()
1236 1236 os.chdir(repo.root)
1237 1237 text = repo.ui.edit("\n".join(edittext), ctx.user())
1238 1238 text = re.sub("(?m)^HG:.*\n", "", text)
1239 1239 os.chdir(olddir)
1240 1240
1241 1241 if not text.strip():
1242 1242 raise util.Abort(_("empty commit message"))
1243 1243
1244 1244 return text
General Comments 0
You need to be logged in to leave comments. Login now